Метаклассы и аннотации
Это две совсем разные темы, если что).
Метаклассы
Предуведомление: Тим Петерс про метаклассы ☺.
Посылка: в питоне всё — объект. Объекты-экземпляры класса конструируются с помощью вызова самого класса. А кто конструирует класс? Мета-класс!
- Внезапно развёрнутое описание на 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__: это метод класса - TODO (проверить!) он не вызывается, если написать C = ctype(…) 
 
 (кажется!) Общая особенность: нельзя написать без наследования от type(), пример отсюда не работает! (кажется!) Общая особенность: нельзя написать без наследования от 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 
Аннотации
Базовая статья: О дисциплине использования аннотаций
Duck typing:
- Экономия кода на описаниях и объявлениях типа
- Экономия (несравненно бо́льшая) кода на всех этих ваших полиморфизмах
- ⇒ Компактный читаемый код, хорошее отношение семантика/синтаксис
- ⇒ Быстрое решение Д/З ☺
Однако:
- Практически все ошибки — runtime
- Много страданий от невнимательности (передал объект не того типа, и не заметил, пока не свалилось) - Вашей невнимательности не поможет даже хитрое IDE: оно тоже не знает о том, какого типа объекты правильные, какого — нет 
- (соответственно, о полях вашего объекта тоже)
 
- Часть прагматики растворяется в коде (например, вы написали строковую функцию, как об этом узнать?) 
- Большие и сильно разрозненные проекты — ?
Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации (annotations)
- Пример аннотаций полей (переменных), параметров и возвращаемых значений 1 class C: 2 A: int = 2 3 N: float 4 5 def __init__(self, param: int = None, signed: bool = True): 6 if param != None: 7 self.A = param if signed else abs(param) 8 9 def mult(self, mlt: int) -> str: 10 return self.A * mlt 11 12 a: C = C(3) 13 b: C = C("QWE") 14 print(f"{a.mult([2])=}, {b.mult(2)=}") 15 print(f"{a.__annotations__=}") 16 print(f"{a.mult.__annotations__=}") 17 print(f"{C.__annotations__}") 18 print(f"{C.__init__.__annotations__}") 19 20 print(a.mult(2)) 21 print(b.mult(2)) 22 print(a.mult("Ho! ")) 23 print(a.N) # an Error! 
- Аннотации сами по себе не влияют на семантику кода - …в т. ч. не занимаются проверкой типов
 
- Аннотации заполняют словарь __annotations__ в соответствующем пространстве имён - …но не они заводят там имена 
 
- Типы в аннотациях — - это настоящие типы — не всегда возможно, например: 
- В действительности могут быть чем угодно (например, строками)
- Можно включить, чтобы вообще всегда были строками (pep-0563): 
 
⇒  Рекомендуется (на момент Python 3.11) использовать именно inspect.get_annotations().
 Рекомендуется (на момент Python 3.11) использовать именно inspect.get_annotations(). 
- Однако eval_str=True — это прямой вызов eval()   
- …как и typing.get_type_hints(C) 
Составные и нечёткие типы
- 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, на семантику работы аннотация не влияет 
 
Модуль typing
- Алиасы (практически typedef), Any, NewType (категоризация), Callable 
- Внезапно полезное: collections.abc - например, как узнать, что нечто — это последовательность: 1 >>> import collections.abc 2 >>> isinstance([1,2,3], collections.abc.Iterable) 3 True 4 >>> isinstance("wer", collections.abc.Iterable) 5 True 6 >>> isinstance((i for i in range(10)), collections.abc.Iterable) 7 True 8 >>> isinstance(1+3j, collections.abc.Iterable) 9 False 10 >>> isinstance("wer", collections.abc.Sequence) 11 True 12 >>> isinstance((i for i in range(10)), collections.abc.Sequence) 13 False 
 
- например, как узнать, что нечто — это последовательность: 
- Инструменты: NoReturn, Union, Optional, Type (если сама переменная — класс), Literal, Final 
- …
- Дженерики (Дженерики, Карл!) 
- Перегрузка функций  Шhат Шhат
Развесистая статья на Хабре (⩽ Python3.8, однако ☺, см pep-0585)
- dataclass — типизированные структуры 
Важно: в Python есть поддержка аннотаций, но в синтаксисе нет их использования.
- Есть в модулях, типа dataclasses) 
⇒ В язык не входит, делайте сами.
MyPy
А вот чем помимо прочего занимается Гвидо в M$
Ещё раз: зачем аннотации?
- Дисциплина программирования - большие, сверхбольшие и «долгие» проекты
 
- Потенциально возможные проверки 
- Прагматика, включенная в синтаксис языка
- Преобразование Python-кода в представления, требующие статической типизации
- …
http://www.mypy-lang.org: статическая типизация в Python (ну, почти… или совсем!)
- Проверка выражений с типизированными данными - В т. ч. не-проверка нетипизиварованных 
 
- Пример:
- Он запускается! Но проверку на статическую типизацию не проходит: 1 $ mypy ex1.py 2 ex1.py:6: error: Unsupported operand types for + ("int" and "str") 3 ex1.py:7: error: Incompatible return value type (got "int", expected "str") 4 ex1.py:13: error: Incompatible types in assignment (expression has type "str", variable has type "int") 5 Found 3 errors in 1 file (checked 1 source file) 6 $ mypy --strict ex1.py 7 ex1.py:1: error: Function is missing a type annotation for one or more arguments 8 ex1.py:3: error: Returning Any from function declared to return "str" 9 ex1.py:6: error: Unsupported operand types for + ("int" and "str") 10 ex1.py:7: error: Incompatible return value type (got "int", expected "str") 11 ex1.py:13: error: Incompatible types in assignment (expression has type "str", variable has type "int") 12 Found 5 errors in 1 file (checked 1 source file) 13 
- Компиляция. Если все объекты полностью типизированы, у них имеется эквивалент в виде соответствующих структур PythonAPI. ЧСХ, у байт-кода тоже есть эквивалент в Python API 
- Таинственный mypyc 
- Пока не рекомендуют использовать, но сами все свои модули им компилируют!
Пример для mypyc: крайне неэффективная реализация чисел Фибоначчи
Сравнение производительности:
Д/З
- Прочитать про: - Метаклассы (см. множество ссылок выше — выберите ту, что попонятнее))
- Аннотации, модули collections.abc и [[py3doc:typing 
- Загляните в Mypy 
 
- EJudge: FloatFix 'Фиксированная точка' Input:- Написать метакласс fixed с параметром ndigits (по умолчанию 3), в котором все возвращаемые обычными (не статическими и не методами класса) методами значения округляются с помощью round() до ndigits знаков после запятой, если они вещественные по определению модуля numbers. Output:- 0.8571 8571/10000 0.8571428571428571428571428571 
- EJudge: InitParam 'Параметры по умолчанию' Input:- Написать метакласс init, который рассчитывает на то, что методы создаваемого им класса полностью аннотированы. Для каждого позиционного параметра обычного метода в этом классе предусматривается значение по умолчанию (если оно не было задано) на основании типа в аннотации. - Если в аннотации тип параметра простой, значение по умолчанию — это тип_пареметра() 
- Если в аннотации тип параметра составной (тип_контейнера[ещё типы], например, list[int]), значение по умолчанию — это тип_контейнера() - Будем считать что тип самой аннотации при этом всегда types.GenericAlias 
 
- Если объект соответствующего типа нельзя создать конструктором без операндов, значение по умолчанию — None 
 Output:- 0/None/[]/defined 1/range(0, 3)/[]/defined 0/range(4, 7)/[]/defined 0/None/[1, 2, 3]/3 
