Метаклассы и сопоставление шаблону
Это две совсем разные темы, если что). Или три, если успеем «Введение в аннотации». TODO А успеем ли?
Не-метаклассы
Частые приёмы программирования:
- Дополнительные действия при изготовлении производного класса
Метод .__init_subclass__()
1 class Titled: 2 def __init_subclass__(cls, title, *args, **kwargs): 3 cls.title = title 4 super().__init_subclass__(*args, **kwargs) 5 def __str__(self): 6 return f"[{self.title}] {super().__str__()}" 7 8 class C(Titled, title="This is C"): 9 pass 10 print(C()) 11 [This is C] <__main__.C object at 0x7fac8261b7d0>
- Объявлению класса можно передавать именные параметры!
__init_subclass__ — это @classmethod (без объявления ☹)
- Интроспекция имени поля в классе:
Метод .__set_name__():
- Вместо
Кстати, а как будут работать два таких дескриптора? Спойлер:
…запихать имя дескриптора в __dict__ и доставать его оттуда явно
- Воспользуемся:
- →
Поля d и e в классе — это дескрипторы, поэтому для чтения / записи используются именно они и их методы .__get__() / .__set__()
Следовательно, поля d и e в .__dict__ объекта не доступны по имени напрямую — но они есть и используются для хранения соответствующих дескрипторам данных
Метаклассы
Предуведомление: Тим Петерс про метаклассы ☺.
Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!
Внезапно развёрнутое описание на StackOverflow (перевод на Хабре)
Забойная статья Sebastian Buczyński 2020 года (Перевод)
Хороший пример real-life кода на Python, эксплуатирующий метаклассы и многое другое:
enum (в частности, How are Enums different?)
Итак, что уже и так может служить конструктором класса?
- Класс можно создать просто функцией
- Декоратором
- Но не т. н. monkey-patch, когда подправляется уже имеющийся класс (⇒ не мы его создаём)
- Класс может быть потомком другого класса, и процесс «создания» — это спецметоды родительского класса.
Зачем тогда нужны ещё отдельные конструкторы классов?
- Чёткого ответа нет.
- Чтобы закрыть дурную бесконечность (кто конструирует конструктор?) — но это ответ на вопрос «почему?», а не «зачем?»
- Чтобы разделить иерархию классов, которой пользуется программист, и то, как конструируется сам базовый класс этой иерархии
«Тонкая настройка» класса к моменту его создания уже произошла, и в самом классе этих инструментов нет
⇒ более чистый mro(), чем в случае наследования
- ⇒ Два похоже работающих класса с общим метаклассом не имеют общего предка
- Чтобы сами метаклассы тоже можно было организовывать в виде дерева наследования
- …
Использование type()
Создание класса с помощью type(name, bases, dict)
это вырожденный вызов type("имя", (кортеж родителей), {пространство имён})
1 C = type("C", (), {})
- Например,
Но type — это просто класс такой ⇒ от него можно унаследоваться, например, перебить ему __init__():
а вот это Boo = overtype… можно записать так:
(по сути, class C: — это class C(metaclass=type):)
(__prepare__() для автоматического создания пространства имён, если есть), __new__(), __init__()
можно перебить ещё __call__ для внесения правок при создании экземпляра класса
__new__()
создаёт экземпляр объекта (а __init__() заполняет готовый)
это метод класса (такой @classmethod без декоратора)
в нём можно поменять всё, что в __init__() приезжает готовое и read-only: __slots__, имя класса (если это метакласс) и т. п.
Общая картина:
1 class ctype(type): 2 3 @classmethod 4 def __prepare__(metacls, name, bases, **kwds): 5 print("prepare", name, bases, kwds) 6 return super().__prepare__(name, bases, **kwds) 7 8 @staticmethod 9 def __new__(metacls, name, parents, ns, **kwds): 10 print("new", metacls, name, parents, ns, kwds) 11 return super().__new__(metacls, name, parents, ns) 12 13 def __init__(cls, name, parents, ns, **kwds): 14 print("init", cls, parents, ns, kwds) 15 return super().__init__(name, parents, ns) 16 17 def __call__(cls, *args, **kwargs): 18 print("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 c = C("100500", base=16)
- →
prepare C (<class 'int'>,) {'parameter': 'See me'} new <class '__main__.ctype'> C (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} {'parameter': 'See me'} init <class '__main__.C'> (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} {'parameter': 'See me'} call <class '__main__.C'> ('100500',) {'base': 16}
Заметим, куда приезжает именной параметр parameter
Особенность __new__: это статический метод, при вызове из super() поле cls надо передавать явно
при этом @staticmethod можно не писать ( это как?)
Особенность __prepare__: это метод класса
Он не вызывается, если написать C = ctype(…). Неизвестно, бага это или фича.
Общая особенность: нельзя написать без наследования от type()
Два примера:
- Ненаследуемый класс
Обратите внимание на параметры 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__ в соответствующем пространстве имён
…но не они заводят сами имена в пространстве имён
- Типы в аннотациях —
это настоящие типы — не всегда возможно, например:
- В действительности могут быть чем угодно (например, строками и любыми другими выражениями 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: MetaBool 'Класс с пустотой'
Написать метакласс empty так, чтобы объекты, порождаемые созданным с его помощью классом, считались пустыми, если хотя бы одно из полей, которое присутствует в __dict__ объекта, пусто. Поля класса проверять не надо.
True True False
EJudge: MatchTurtle 'Интерпретатор с черепашкой'
С помощью конструкции match / case (и только её, условные операторы и иные конструкции с if в этой задачи не разрешены) написать программу, которая в цикле вводит и интерпретирует перечисленные ниже команды перемещения «черепашки» по координатной плоскости. Конец ввода — пустая строка. Изначально черепашка находится в точке 0, 0. Все «слова» в команде разделены ровно одним пробелом.
move направление, где направление — это s, n, w или е: переместить черепашку на один шаг вниз, вверх, влево или вправо соответственно.
move: переместить черепашку в том же направлении, указанном последней командой вида move направление. Если такой команды ещё не было, черепашка не перемещается.
Все остальные команды вида move что-то там должны выводить текст Cannot move to что-то там, и не перемещать черепашку
retreat: переместить черепашку в направлении, обратном последней команде вида move направление. Если такой команды ещё не было, черепашка не перемещается.
info что, где что — это x , y или xy: вывести абсциссу, ординату или пару «абсцисса ордината»
say какое-то сообщение вывести «какое-то сообщение» (включая пустое)
- все остальные команды игнорируются
Перед выходом из программы дополнительно выполняется команда info xy.
say Hello, world! move n move e jump info xy move n move base look around info x retreat retreat info y
Hello, world! 1 1 Cannot move to base 1 0 1 0
EJudge: MetaPosition 'Метакласс с заготовками'
Написать метакласс positioned, который добавляет в создаваемый с его помощью класс три свойства:
Строковое представление экземпляра этого класса должно выглядеть как "поле1=значение1 поле2=значение2 …" для всех аннотированных полей этого класса (в порядке их появления в аннотации).
- При создании экземпляра класса ему можно передавать произвольное количество параметров (включая ноль). Первый параметр инициализирует первое аннотированное поле в этом экземпляре, второй — второе и т. д.; если параметров больше, чем аннотированных полей, они отбрасываются
- При сопоставлении шаблону допускается позиционное сопоставление с аннотированными полями (в порядке появления в аннотации)
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