Метаклассы и сопоставление шаблону
Это две совсем разные темы, если что). Или три, если успеем «Введение в аннотации». TODO А успеем ли?
Не-метаклассы
Частые приёмы программирования:
- Дополнительные действия при изготовлении производного класса
Метод .__init_subclass__()
- →
[This is C] <__main__.C object at 0x7fac8261b7d0>
- Объявлению класса можно передавать именные параметры!
__init_subclass__ — это @classmethod, но декоратор можно не писать (
как это o_O)
- Интроспекция имени поля в классе:
Метод .__set_name__():
(был на позапрошлой лекции)
Метаклассы
Предуведомление: Тим Петерс про метаклассы ☺:
Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t
Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!
Внезапно развёрнутое описание на StackOverflow (перевод на Хабре)
Забойная статья Sebastian Buczyński 2020 года (Перевод)
Хороший пример real-life кода на Python, эксплуатирующий метаклассы и многое другое:
enum (в частности, How are Enums different?)
Итак, что уже и так может служить конструктором класса?
Просто функция (создали class:, вернули)
- Декоратор (он же и так функция). Да, декораторы можно применять к классам.
- Но не т. н. monkey-patch, когда подправляется уже имеющийся класс (⇒ не мы его создаём)
Класс может быть потомком другого класса, и процесс «создания» — это спецметоды родительского класса, .__init_subclass__() и ему подобные
Зачем тогда нужны ещё отдельные конструкторы классов?
- Чёткого ответа нет.
- Потому что надо было пресечь дурную бесконечность (кто конструирует конструктор?) — но это ответ на вопрос «почему?», а не «зачем?»
Чтобы отделить иерархию классов, которой пользуется программист, от того, как конструируется сам базовый класс этой иерархии
«Тонкая настройка» класса к моменту его создания уже произошла, и в самом классе этих инструментов нет
⇒ более чистый mro(), чем в случае наследования
Два одинаково работающих класса с общим метаклассом не имеют общего предка кроме Адамаclass 'object'
- ⇒ прямое управление наследованием
Чтобы сами метаклассы тоже можно было организовывать в виде дерева наследования, т. е. применять объектное планирование к конструкторам классов
- …
Использование type()
Создание класса с помощью type("имя", (кортеж родителей), {пространство имён}`)
- Например,
Но type — это просто класс такой
Кстати, дурная бесконечность пресекается до метаклассов!
⇒ от него можно унаследоваться, например, перебить ему __init__():
а вот это Boo = overtype… можно записать так:
(⇒ по сути, class C: — это class C(metaclass=type):)
Подробности и основные спецметоды:
__prepare__() для автоматического создания пространства имён
возвращает dict (или ему подобное)
__new__() создаёт экземпляр объекта (а __init__() заполняет готовый)
в нём можно поменять всё, что в __init__() приезжает готовое и read-only: __slots__, имя класса (если это метакласс) и т. п.
- возвращает объект
Можно задавать и для обычного (не мета-) класса
__init__(self) — это обычный init, но self-то в нём — это экземпляр метакласса, то есть наш создаваемый класс. Вызывается в момент описания нашего класса.
__call__() — это тоже обычный __call__, и тоже для нашего класса, то есть то, что вызывается во время создания экземпляра (класс(…))
Общая картина:
1 from pprint import pprint 2 3 class ctype(type): 4 @classmethod 5 def __prepare__(metacls, name, bases, **kwds): 6 pprint(("prepare", name, bases, kwds)) 7 return super().__prepare__(name, bases, **kwds) 8 9 def __new__(metacls, name, parents, ns, **kwds): 10 pprint(("new", metacls, name, parents, ns, kwds)) 11 return super().__new__(metacls, name, parents, ns) 12 13 def __init__(cls, name, parents, ns, **kwds): 14 pprint(("init", cls, parents, ns, kwds)) 15 return super().__init__(name, parents, ns) 16 17 def __call__(cls, *args, **kwargs): 18 pprint(("call", cls, args, kwargs)) 19 return super().__call__(*args, **kwargs) 20 21 class C(int, metaclass=ctype, parameter="See me"): 22 field = 42 23 24 print("Create an instance:") 25 c = C("100500", base=16) 26 print(c, c % 256, type(c), type(c % 256))
- →
('prepare', 'C', (<class 'int'>,), {'parameter': 'See me'}) ('new', <class '__main__.ctype'>, 'C', (<class 'int'>,), {'__firstlineno__': 26, '__module__': '__main__', '__qualname__': 'C', '__static_attributes__': (), 'field': 42}, {'parameter': 'See me'}) ('init', <class '__main__.C'>, (<class 'int'>,), {'__firstlineno__': 26, '__module__': '__main__', '__qualname__': 'C', '__static_attributes__': (), 'field': 42}, {'parameter': 'See me'}) Create an instance: ('call', <class '__main__.C'>, ('100500',), {'base': 16}) 1049856 0 <class '__main__.C'> <class 'int'>Заметим, куда приезжает именной параметр parameter
Особенность __new__: это статический метод, при вызове из super() поле cls надо передавать явно
при этом @staticmethod можно не писать
Особенность __prepare__: это метод класса
Он не вызывается, если написать C = ctype(…). Неизвестно, бага это или фича.
Общая особенность: нельзя написать свой собственный метакласс без наследования от type()
Всё та же проблема: проксирование в питоне — сложная задача (c % 256 — это int)
Два примера:
- Ненаследуемый класс
Обратите внимание на параметры super() —
- Синглтон
1 class Singleton(type): 2 _instance = None 3 def __call__(cls, *args, **kw): 4 if cls._instance is None: 5 cls._instance = super().__call__(*args, **kw) 6 return cls._instance 7 8 class S(metaclass=Singleton): 9 A = 3 10 s, t = S(), S() 11 s.newfield = 100500 12 print(f"{s.newfield=}, {t.newfield=}") 13 print(f"{s is t=}")
Модуль types
Сопоставление шаблону
Базовая статья: pep-636 (а также pep-635 и pep-634)
Главная сложность: конструкция match … case имеет отличный от Python синтаксис! Спасибо смене парсера с LL(1) на PEG.
Пересказ tutorial:
Вместо цепочки однотипных elif-ов
- →
- Связанные переменные
- Распаковка и catch-all:
Распаковка, как всегда, включая len()==0
- Альтернативы и явно связанные переменные
- Фильтры:
Проверка типов (help(complex)), проверка полей объекта (как правило по имени, редко когда определено перечисление полей)
Здесь x — связанная переменная заданного типа
- … но можно обойтись и без неё
- Экземпляр класса определяется перечислением полей поимённо или (если задано) позиционно:
1 from collections import namedtuple 2 C = namedtuple("C", "a b") 3 for c in C(2, 3), C(1, 2), C(2, 1), C(42, 100500), C(-1, -1): 4 match c: 5 case C(2, 3): # Позиционное перечисление 6 print(C, "with 2 and 3") 7 case C(a=1, b=V) | C(a=V, b=1): # Поимённое перечисление, одна переменная связана 8 print(C, "with 1 and", V) 9 case C(42): # Позиционное задание только одного поля 10 print("Special", C) 11 case C(A, b=B): # Одна переменная связана позиционно, другая — именем 12 print("Any", C, "with", A, "and", B)
Не обязательно задавать все поля
- Можно смешивать позиционное и именное перечисление / связывание
Позиционное перечисление полей можно определить вручную спецполем __match_args__:
- Словари:
- Как отличить константу от связанной переменной?
Никак! Храните константы в изолированных пространствах имён: Здесь Color.RED воспринимается как константа, а WHITE — как связанная переменная (старались, задавали, а значение-то и поменялось ☺)
Введение в аннотации
Базовая статья: О дисциплине использования аннотаций
Duck typing:
- Экономия кода на описаниях и объявлениях типа
- Экономия (несравненно бо́льшая) кода на всех этих ваших полиморфизмах
- ⇒ Компактный читаемый код, хорошее отношение семантика/синтаксис
- ⇒ Быстрое решение Д/З ☺
Однако:
- Практически все ошибки — runtime
- Много страданий от невнимательности (передал объект не того типа, и не заметил, пока не свалилось)
Вашей невнимательности не поможет даже хитрое IDE: оно тоже не знает о том, какого типа объекты правильные, какого — нет
- (соответственно, о полях вашего объекта тоже)
Часть прагматики растворяется в коде (например, вы написали строковую функцию, как об этом узнать?)
- Большие и сильно разрозненные проекты — ?
Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации (annotations)
Аннотации — часть синтаксиса Python
Аннотации не влияют на семантику непосредственно: наличие или отсутствие аннотации не меняет дальнейшую работу интерпретатора, но можно исследовать их как данные
Пример аннотаций полей (переменных), параметров и возвращаемых значений
1 import inspect 2 3 class C: 4 A: int = 2 5 N: float 6 7 def __init__(self, param: int = None, signed: bool = True): 8 if param != None: 9 self.A = param if signed else abs(param) 10 11 def mult(self, mlt: int) -> str: 12 return self.A * mlt 13 14 a: C = C(3) 15 b: C = C("QWE") 16 print(f"{a.mult([2])=}, {b.mult(2)=}") 17 print(f"{inspect.get_annotations(a.mult)=}") 18 print(f"{inspect.get_annotations(C.mult)=}") 19 print(f"{inspect.get_annotations(C)}") 20 print(f"{inspect.get_annotations(C.__init__)}") 21 22 print(a.mult(2)) 23 print(b.mult(2)) 24 print(a.mult("Ho! ")) 25 print(a.N) # an Error!
- Аннотации сами по себе не влияют на семантику кода (умножение строки сработало)
- …в т. ч. не занимаются проверкой типов
Аннотации заполняют словарь __annotations__ в соответствующем пространстве имён
…но не они заводят сами имена в пространстве имён (см. N в примере)
Типы в аннотациях — это настоящие типы
TODO Python3.14+ annotationlib и отложенные аннотации
New: Python3.14+ До этого было невозможно, поэтому придумали т. н. «строковые аннотации» (pep-0563), но, надеюсь, их задепрекейтят
Тем не менее это пока используется, например, в ptpython ☹
- В действительности могут быть чем угодно (например, строками и любыми другими выражениями Python)
Составные и нечёткие типы
pep-0585: Во многих случаях можно писать что-то вроде list[int]
1 >>> def fun(lst: list[int]): pass 2 >>> inspect.get_annotations(fun) 3 {'lst': list[int]} 4 >>> inspect.get_annotations(fun)['lst'] 5 list[int] 6 >>> type(inspect.get_annotations(fun)['lst']) 7 <class 'types.GenericAlias'> 8 >>> ann = inspect.get_annotations(fun)['lst'] 9 >>> typing.get_args(ann) 10 (<class 'int'>,) 11 >>> typing.get_origin(ann) 12 <class 'list'> 13
.get_args() возвращает кортеж с аннотациями элемента, .get_origin() — тип контейнера
Again, на семантику работы аннотация не влияет
Более полная лекция по использованию аннотаций для статической типизации в Python планируется в допглавах магистерского курса.
Д/З
- Прочитать про:
- Метаклассы (см. множество ссылок выше — выберите ту, что попонятнее))
EJudge: Defauter 'Значения по умолчанию'
Input:Написать класс Defaulter, потомки которого будут обладать следующим свойством: для всех полей, аннотацией которых является тип, поле класса будет проинициализировано значением тип(). Использовать .__init_subclass__()
Output:0 0.0 False 0 0.0 False
EJudge: ClassCounter 'Счётчик классов'
Input:Написать класс Generative, который, если его использовать как метакласс, добавляет в порождаемый с его помощью класс @property-дескриптор .generation. В нём хранится константа — количество порождённых с помощью Generative классов (удаление классов не отслеживается). Сеттер и делитер для generation делать не надо, соответствующие действия должны вызывать исключения. Поле .generation также должно присутствовать и в экземплярах, однако допустимо, чтобы его можно было удалять или изменять без ущерба для основного дескриптора (соответствующих тестов не будет).
Output:1 1 2 2 2 2
EJudge: MetaPosition 'Метакласс с заготовками'
Input:Написать метакласс positioned, который добавляет в создаваемый с его помощью класс три свойства:
Строковое представление экземпляра этого класса должно выглядеть как "поле1=значение1 поле2=значение2 …" для всех аннотированных полей этого класса (в порядке их появления в аннотации).
- При создании экземпляра класса ему можно передавать произвольное количество параметров (включая ноль). Первый параметр инициализирует первое аннотированное поле в этом экземпляре, второй — второе и т. д.; если параметров больше, чем аннотированных полей, они отбрасываются
- При сопоставлении шаблону допускается позиционное сопоставление с аннотированными полями (в порядке появления в аннотации)
(TODO в тесты) Если соответствующего поля в объекте нет (потому что в классе была только аннотация, а значения не было), при доступе к нему естественным образом возникает исключение
Output:a=1 b=42.0 C1 42.0 a=4 b=42.0 C42 4 a=100.0 b=500 C100500 a=7 b=2 C a=7 b=2
EJudge: AbsoluteMeta 'Метакласс c модулем'
Input:Написать класс Absolute, который можно использовать как метакласс. Absolute добавляет в порождаемый класс дескриптор abs и метод __abs__(). При создании класса ему можно передавать два именных параметра: width — имя поля «ширина» и height — имя поля «высота». По умолчанию width="width" и height="height". Создание полей abs и __abs__ происходит по следующим правилам (правила применяются по принципу «первое подходящее»):
Если метод __abs__() существует, он не меняется; если это не метод — поле заменяется на метод
Если существует метод abs() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое
Если существует метод __len__() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое
Если существуют методы «ширина»() и «высота»() и они допускают вызов без параметров, то __abs__() возвращает их произведение
Если в классе существуют не-callable поля «ширина» и «высота», то __abs__() возвращает их произведение
В противном случае __abs__() возвращает сам объект без изменений
Дескриптор abs создаётся всегда (в том числе вместо любого атрибута abs, если он был): возвращает __abs__().
Output:2 64
