Метаклассы и сопоставление шаблону
Это две совсем разные темы, если что). Или три, если успеем «Введение в аннотации». 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__():
(был на позапрошлой лекции)
Метаклассы
Предуведомление: Тим Петерс про метаклассы ☺.
Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!
Внезапно развёрнутое описание на StackOverflow (перевод на Хабре)
Забойная статья Sebastian Buczyński 2020 года (Перевод)
Хороший пример real-life кода на Python, эксплуатирующий метаклассы и многое другое:
enum (в частности, How are Enums different?)
Итак, что уже и так может служить конструктором класса?
- Просто функция
- Декоратор
- Но не т. н. monkey-patch, когда подправляется уже имеющийся класс (⇒ не мы его создаём)
- Класс может быть потомком другого класса, и процесс «создания» — это спецметоды родительского класса.
Зачем тогда нужны ещё отдельные конструкторы классов?
- Чёткого ответа нет.
- Чтобы закрыть дурную бесконечность (кто конструирует конструктор?) — но это ответ на вопрос «почему?», а не «зачем?»
Чтобы отделить иерархию классов, которой пользуется программист, от того, как конструируется сам базовый класс этой иерархии
«Тонкая настройка» класса к моменту его создания уже произошла, и в самом классе этих инструментов нет
⇒ более чистый mro(), чем в случае наследования
- ⇒ Два похоже работающих класса с общим метаклассом не имеют общего предка
- Чтобы сами метаклассы тоже можно было организовывать в виде дерева наследования
- …
Использование type()
Создание класса с помощью type("имя", (кортеж родителей), {пространство имён}`)
- Например,
Но type — это просто класс такой ⇒ от него можно унаследоваться, например, перебить ему __init__():
а вот это Boo = overtype… можно записать так:
(⇒ по сути, class C: — это class C(metaclass=type):)
(__prepare__() для автоматического создания пространства имён, если есть), __new__(), __init__()
можно перебить ещё __call__ для внесения правок при создании экземпляра класса
__new__()
создаёт экземпляр объекта (а __init__() заполняет готовый)
в нём можно поменять всё, что в __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: RndSwissknife 'Случайности'
Написать с использованием оператора match / case функцию rnd() от двух параметров (a и b=None), которая возвращает некоторое случайное значение в зависимости от того, какого типа и в каком количестве переданы ей параметры:
целое → случайное целое в диапазоне [0…a]
два целых → случайное целое в диапазоне [a…b]
вещественное и целое/вещественное → случайное вещественное в диапазоне [a…b[
индексируемая или итерируемая последовательность → случайный элемент a
индексируемая или итерируемая последовательность и целое → список из b случайных элементов a
строка и целое → случайная подстрока a длиной b
строка → случайное слово из a.split()
строка и строка → случайная строка из a.split(b)
В решении важно использовать только необходимые функции random, иначе тесты не пройдут.
1 import random 2 random.seed(123) 3 print(*(rnd(3, 5) for i in range(11))) 4 prnit(rnd(5)) 5 print(*(round(rnd(3., 5), 4) for i in range(4))) 6 print(*(rnd("Substring", 4) for i in range(4))) 7 print(*(rnd("We won oewn wow") for i in range(5))) 8 print(*(rnd("We won oewn wow", "wo") for i in range(4))) 9 print(*(rnd(range(10)) for i in range(12))) 10 print(*(rnd({1, 3, 5, 7}) for i in range(12))) 11 print(*(rnd(range(10), 3) for i in range(4))) 12 print(*(rnd(enumerate("qwe"), 1) for i in range(5)))
3 4 3 4 4 3 3 4 5 5 4 2 4.7042 3.3193 3.6744 3.6676 ubst ubst Subs stri We wow We We oewn w n oewn We We 1 2 2 0 4 6 9 7 4 7 0 4 5 7 3 5 1 7 7 7 7 5 1 3 [7, 0, 4] [2, 3, 5] [8, 7, 3] [7, 0, 5] [(2, 'e')] [(1, 'w')] [(2, 'e')] [(2, 'e')] [(1, 'w')]
EJudge: MatchSquare 'Прямоугольный класс'
Напишите (в очередной раз ☹) класс Square(x, y, w) со следующими свойствами:
x и y — это координаты левой нижней вершины квадрата, а w — его ширина
Дополнительно поддерживаются поля h (равное w), s (равное площади квадрата) и center (кортеж с координатами середины квадрата)
Все поля, кроме s, можно менять
- попытка изменить площадь ни к чему не приводит ничего не происходит
- изменение центра или координат вершины влияют друг на друга, а ширина и высота остаются прежней
- изменение ширины или высоты не влияют на координаты вершины, но сдвигают центр
Поле center должно поддерживать операцию добавления кортежа из двух чисел — это смещение середины квадрата
При сопоставлении в операторе match / case все эти поля можно использовать как именные параметры, а x, y и w — как позиционные
1 for x, y, w in (1, 2, 0), (1, 1, 7), (3, 4, 10), (5, 3, 6): 2 Sq = Square(x, y, w) 3 Sq.center += -1, -1 4 match Sq: 5 case Square(_, _, 0): 6 print("Zero square") 7 case Square(0, 0, _): 8 print("Started from 0") 9 case Square(s=100): 10 print("10x10 square") 11 case Square(center=c) if c[0] == round(c[0]) and c[1] == round(c[1]): 12 print("Even-sized square")
Zero square Started from 0 10x10 square Even-sized square
EJudge: AnnoDoc 'Документация в аннотациях'
написать мета-класс AnnoDoc, который будет добавлять в произведённые им классы такое свойство:
- если data-атрибут класса аннотирован, и эта аннотация — строка, она считается его документацией
- если у класса уже есть документация — добавляется туда с новой строки
- если у класса не было документации — становится первой строкой документации класса
- если у аннотированного строкой атрибута есть значение, тип этого значения становится его аннотацией
- если аннотированный строкой атрибут ещё не задан, аннотация удаляется
Модифицировать поля .__doc__ и .__annotations__ (в обход inspect) разрешается.
This is a class a: A variable b: Undefiled field {'a': <class 'int'>, 'с': <class 'int'>}
- если data-атрибут класса аннотирован, и эта аннотация — строка, она считается его документацией