
__subclasshook__ — один из моих любимых элементов Python. Абстрактные базовые классы (ABC — Abstract Base Class) с помощью __subclasshook__ могут указывать, что считается подклассом ABC, даже если целевой класс не знает об ABC:
class PalindromicName(ABC): @classmethod def __subclasshook__(cls, C): name = C.__name__.lower() return name[::-1] == name class Abba: ... class Baba: ... >>> isinstance(Abba(), PalindromicName) True >>> isinstance(Baba(), PalindromicName) False
Странные вещи можно делать с этим __subclasshook__. Ещё в 2019 году я использовал его для создания немонотонных типов, где что—то считается NotIterable, когда не имеет метода __iter__. И не было ничего слишком уж дьявольского, что можно было с этим сделать, ведь ничто в Python не взаимодействовало с ABC. И это сокращало ущерб коду в продакшене.
Но в Python 3.10 добавили сопоставление с образцом.
Краткий обзор сопоставления с образцом
Из туториала:
match command.split(): case ["quit"]: print("Goodbye!") quit_game() case ["look"]: current_room.describe() case ["get", obj]: character.get(obj, current_room)
Сопоставлять можно массивы, словари и пользовательские объекты. Для поддержки сопоставления объектов используется isinstance(obj, class). Этот код проверяет случаи, когда:
-
objимеет типclass; -
obj— транзитивный подтипclass; -
class— это ABC и он определяет__subclasshook__, соответствующий типуobj.
Это заставило меня задуматься, способны ли ABC «угнать» сопоставление с образцом:
from abc import ABC class NotIterable(ABC): @classmethod def __subclasshook__(cls, C): return not hasattr(C, "__iter__") def f(x): match x: case NotIterable(): print(f"{x} is not iterable") case _: print(f"{x} is iterable") if __name__ == "__main__": f(10) f("string") f([1, 2, 3])
Конечно, Python прекратит эти придирки, правильно?
$ py10 abc.py 10 is not iterable string is iterable [1, 2, 3] is iterable
Ох…
Сделаем хуже
Сопоставление с образцом может деструктурировать поля объекта:
match event.get(): case Click(position=(x, y)): handle_click_at(x, y)
Получить поле можно только после определения объекта. Если не использовать ABC, то не сопоставляется «любой объект, у которого есть поле foo»:
from abc import ABC from dataclasses import dataclass from math import sqrt class DistanceMetric(ABC): @classmethod def __subclasshook__(cls, C): return hasattr(C, "distance") def f(x): match x: case DistanceMetric(distance=d): print(d) case _: print(f"{x} is not a point") @dataclass class Point2D: x: float y: float @property def distance(self): return sqrt(self.x**2 + self.y**2) @dataclass class Point3D: x: float y: float z: float @property def distance(self): return sqrt(self.x**2 + self.y**2 + self.z**2) if __name__ == "__main__": f(Point2D(10, 10)) f(Point3D(5, 6, 7)) f([1, 2, 3])
14.142135623730951 10.488088481701515 [1, 2, 3] is not a point
Теперь лучше! ABC разрешает вопрос сопоставления, а объект — вопрос деструктурирования, то есть можно сделать так:
def f(x): match x: case DistanceMetric(z=3): print(f"A point with a z-coordinate of 3") case DistanceMetric(z=z): print(f"A point with a z-coordinate that's not 3") case DistanceMetric(): print(f"A point without a z-coordinate") case _: print(f"{x} is not a point")
Комбинаторы
Сопоставление с образцом — это гибкий, но и достаточно ограниченный инструмент: оно возможно только с типом объекта. А значит, отдельный ABC нужно писать для всего, что мы должны тестировать. К счастью, это ограничение можно обойти: типизация в Python динамическая; в 99% случаев это значит: «вам не нужны статические типы, если вы не против, чтобы во время выполнения что—нибудь упало». Но ещё это означает, что информация о типах существует во время выполнения, и во время выполнения типы могут создаваться.
Можно ли воспользоваться этим при сопоставлении с образцом? Давайте попробуем:
def Not(cls): class _Not(ABC): @classmethod def __subclasshook__(_, C): return not issubclass(C, cls) return _Not def f(x): match x: case Not(DistanceMetric)(): print(f"{x} is not a point") case _: print(f"{x} is a point")
Not принимает класс, определяет новый ABC, устанавливает для этого ABC хук на «всё, что не относится к классу», — и возвращает этот ABC:
case Not(DistanceMetric)(): ^ SyntaxError: expected ':'
Ошибка! Наконец, мы добрались до предела сопоставления с образцом в ABC. Но это «просто» синтаксическая ошибка:
+ n = Not(DistanceMetric) match x: - case Not(DistanceMetric)(): + case n():
PlanePoint(x=10, y=10) is a point SpacePoint(x=5, y=6, z=7) is a point [1, 2, 3] is not a point
Получилось! И просто для проверки напишем And:
from abc import ABC from dataclasses import dataclass from collections.abc import Iterable def Not(cls): class _Not(ABC): @classmethod def __subclasshook__(_, C): return not issubclass(C, cls) return _Not def And(cls1, cls2): class _And(ABC): @classmethod def __subclasshook__(_, C): return issubclass(C, cls1) and issubclass(C, cls2) return _And def f(x): n = And(Iterable, Not(str)) match x: case n(): print(f"{x} is a non-string iterable") case str(): print(f"{x} is a string") case _: print(f"{x} is a string or not-iterable") if __name__ == "__main__": f("abc") f([1, 2, 3])
Работает, """как ожидается""".
Всем заправляет кеширование
Это заставило меня задуматься: «Что, если функция __subclasshook__ не была бы чистой?» Можно ли написать ABC, соответствующий первому, но не последующему переданному типу?
from abc import ABC class OneWay(ABC): seen_classes = set() @classmethod def __subclasshook__(cls, C): print(f"trying {C}") if C in cls.seen_classes: return False cls.seen_classes |= {C} return True def f(x): match x: case OneWay(): print(f"{x} is a new class") case _: print(f"we've seen {x}'s class before") if __name__ == "__main__": f("abc") f([1, 2, 3]) f("efg")
Увы, всё бесполезно:
trying <class 'str'> abc is a new class trying <class 'list'> [1, 2, 3] is a new class efg is a new class
Похоже, __subclasshook__ кеширует результаты проверки заданного типа. CPython исходит из того, что люди не хотят зашивать побочные эффекты в эзотерические уголки языка. Посмотрим, как много известно им:
class FlipFlop(ABC): flag = False @classmethod def __subclasshook__(cls, _): cls.flag = not cls.flag return cls.flag
Поразвлекаться с побочными эффектами по-прежнему нельзя. Этот ABC пропускает все другие типы.
А этот ABC спрашивает пользователя, что он должен делать с каждым типом:
class Ask(ABC): first_class = None @classmethod def __subclasshook__(cls, C): choice = input(f"hey should I let {C} though [y/n] ") if choice == 'y': print("okay we'll pass em through") return True return False
Попробуйте эти два ABC выше в сопоставлении с образцом. Они работают!
Должен ли я этим пользоваться?
Нет. В целом функция сопоставления с образцом спроектирована достаточно разумно, и люди ожидают, что она будет разумно вести себя, а __subclasshook__ — это очень чёрная магия. Уловки с ним могут иметь место в тёмном сердце сложной библиотеки, но, безусловно, не для кода, с которым ваши коллеги будут иметь дело каждый день. Так что да, ничего полезного вы не узнали. Я просто люблю жуткие штуки.
-
Декоратор
@propertyделаетdistanceдоступным для чтения и записи в качестве атрибута: вместоpoint.distance()с ним можно написатьpoint.distance, и__subclasshook__будет проще понять эту конструкцию.
А мы поможем прокачать ваши навыки или с самого начала освоить профессию, актуальную в любое время:
Выбрать другую востребованную профессию.

ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/680744/
Добавить комментарий