Метаклассы и сопоставление шаблону

Это две совсем разные темы, если что). Или три, если успеем «Введение в аннотации». TODO А успеем ли?

Не-метаклассы

Частые приёмы программирования:

Метаклассы

Предуведомление: Тим Петерс про метаклассы ☺:

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t

Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!

Хороший пример real-life кода на Python, эксплуатирующий метаклассы и многое другое:

Итак, что уже и так может служить конструктором класса?

Зачем тогда нужны ещё отдельные конструкторы классов?

  1. Чёткого ответа нет.
  2. Потому что надо было пресечь дурную бесконечность (кто конструирует конструктор?) — но это ответ на вопрос «почему?», а не «зачем?»
  3. Чтобы отделить иерархию классов, которой пользуется программист, от того, как конструируется сам базовый класс этой иерархии

    • «Тонкая настройка» класса к моменту его создания уже произошла, и в самом классе этих инструментов нет

    • ⇒ более чистый mro(), чем в случае наследования

    • Два одинаково работающих класса с общим метаклассом не имеют общего предка кроме Адамаclass 'object'

    • ⇒ прямое управление наследованием
  4. Чтобы сами метаклассы тоже можно было организовывать в виде дерева наследования, т. е. применять объектное планирование к конструкторам классов

Использование type()

Подробности и основные спецметоды:

Общая картина:

Два примера:

Сопоставление шаблону

Базовая статья: pep-636 (а также pep-635 и pep-634)

Главная сложность: конструкция match … case имеет отличный от Python синтаксис! Спасибо смене парсера с LL(1) на PEG.

Пересказ tutorial:

Введение в аннотации

Базовая статья: О дисциплине использования аннотаций

Duck typing:

Однако:

Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации (annotations)

Пример аннотаций полей (переменных), параметров и возвращаемых значений

Составные и нечёткие типы

составные типы:

Более полная лекция по использованию аннотаций для статической типизации в Python планируется в допглавах магистерского курса.

Д/З

  1. Прочитать про:
  2. EJudge: Defauter 'Значения по умолчанию'

    Написать класс Defaulter, потомки которого будут обладать следующим свойством: для всех полей, аннотацией которых является тип, поле класса будет проинициализировано значением тип(). Использовать .__init_subclass__()

    Input:

       1 class C(Defaulter):
       2     A: int 
       3     B: float = 123.456
       4     C: "Not a type"
       5 
       6 a = C() 
       7 print(C.A, C.B, hasattr(C, "C"), a.A, a.B, hasattr(a, "C"))
    
    Output:

    0 0.0 False 0 0.0 False
  3. EJudge: ClassCounter 'Счётчик классов'

    Написать класс Generative, который, если его использовать как метакласс, добавляет в порождаемый с его помощью класс @property-дескриптор .generation. В нём хранится константа — количество порождённых с помощью Generative классов (удаление классов не отслеживается). Сеттер и делитер для generation делать не надо, соответствующие действия должны вызывать исключения. Поле .generation также должно присутствовать и в экземплярах, однако допустимо, чтобы его можно было удалять или изменять без ущерба для основного дескриптора (соответствующих тестов не будет).

    Input:

       1 class A(metaclass=Generative):
       2     pass
       3 
       4 print(A.generation, A().generation)
       5 
       6 class B(metaclass=Generative):
       7     pass
       8 
       9 print(B.generation, A.generation, B().generation, A().generation)
    
    Output:

    1 1
    2 2 2 2
  4. EJudge: MetaPosition 'Метакласс с заготовками'

    Написать метакласс positioned, который добавляет в создаваемый с его помощью класс три свойства:

    • Строковое представление экземпляра этого класса должно выглядеть как "поле1=значение1 поле2=значение2 …" для всех аннотированных полей этого класса (в порядке их появления в аннотации).

    • При создании экземпляра класса ему можно передавать произвольное количество параметров (включая ноль). Первый параметр инициализирует первое аннотированное поле в этом экземпляре, второй — второе и т. д.; если параметров больше, чем аннотированных полей, они отбрасываются
    • При сопоставлении шаблону допускается позиционное сопоставление с аннотированными полями (в порядке появления в аннотации)
    • (TODO в тесты) Если соответствующего поля в объекте нет (потому что в классе была только аннотация, а значения не было), при доступе к нему естественным образом возникает исключение

    Input:

       1 class C(metaclass=positioned):
       2     a: int = 1
       3     b: float = 42.0
       4 
       5 for c in C(), C(4), C(100.0, 500), C(7, 2):
       6     print(c)
       7     match c:
       8         case C(1):
       9             print("C1", c.b)
      10         case C(b=42):
      11             print("C42", c.a)
      12         case C(100, 500):
      13             print("C100500")
      14         case C():
      15             print("C", c)
    
    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
  5. EJudge: AbsoluteMeta 'Метакласс c модулем'

    Написать класс Absolute, который можно использовать как метакласс. Absolute добавляет в порождаемый класс дескриптор abs и метод __abs__(). При создании класса ему можно передавать два именных параметра: width — имя поля «ширина» и height — имя поля «высота». По умолчанию width="width" и height="height". Создание полей abs и __abs__ происходит по следующим правилам (правила применяются по принципу «первое подходящее»):

    • Если метод __abs__() существует, он не меняется; если это не метод — поле заменяется на метод

    • Если существует метод abs() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое

    • Если существует метод __len__() и этот метод допускает вызов без параметров, то __abs__() должен делать то же самое

    • Если существуют методы «ширина»() и «высота»() и они допускают вызов без параметров, то __abs__() возвращает их произведение

    • Если в классе существуют не-callable поля «ширина» и «высота», то __abs__() возвращает их произведение

    • В противном случае __abs__() возвращает сам объект без изменений

    Дескриптор abs создаётся всегда (в том числе вместо любого атрибута abs, если он был): возвращает __abs__().

    Input:

       1 class D(metaclass=Absolute):
       2     def __len__(self):
       3         return 2
       4 
       5 class E(metaclass=Absolute, height="depth"):
       6     width = depth = 0
       7     def __init__(self, sz):
       8         self.width = self.depth = sz
       9 
      10 print(abs(D()), abs(E(8))) 
    
    Output:

    2 64

LecturesCMC/PythonIntro2025/12_MetaclassMatch (последним исправлял пользователь FrBrGeorge 2025-11-24 15:18:52)