Метаклассы и аннотации
Это две совсем разные темы, если что).
Метаклассы
Внезапно развёрнутое описание на 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 def __call__(self, *args, **kwargs): 4 print("call", self, args, kwargs) 5 return super().__call__(*args, **kwargs) 6 7 def __new__(cls, name, parents, ns): 8 print("new", cls, name, parents, ns) 9 return super().__new__(cls, name, parents, ns) 10 11 def __init__(self, name, parents, ns): 12 print("init", self, parents, ns) 13 return super().__init__(name, parents, ns) 14 15 class C(int, metaclass=ctype): 16 field = 42 17 18 c = C("100500", base=16)
- →
new <class '__main__.ctype'> C (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} init <class '__main__.C'> (<class 'int'>,) {'__module__': '__main__', '__qualname__': 'C', 'field': 42} call <class '__main__.C'> ('100500',) {'base': 16}
Особенность: __new__ — это метод класса: при вызове из super() поле cls надо передавать явно
Два примера:
- Ненаследуемый класс
Обратите внимание на параметры 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: оно тоже не знает о том, какого типа объекты правильные, какого — нет
- (соответственно, о полях вашего объекта тоже)
Часть прагматики растворяется в коде (например, вы написали строковую функцию, как об этом узнать?)
- Большие и сильно разрозненные проекты — ?
Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации
- Пример аннотаций полей (переменных), параметров и возвращаемых значений
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__ в соответствующем пространстве имён
…но не они заводят там имена
- Типы в аннотациях —
это настоящие типы (Python ⩽ 3.9)
это строки (Python 3.10+)
Составные и нечёткие типы
pep-0585: Во многих случаях можно писать что-то вроде list[int]
- Again, на семантику работы аннотация не влияет
Модуль typing
Алиасы (практически typedef), Any, NewType (категоризация), Callable
Дженерики и collections.abc
Инструменты: NoReturn, Union, Optional, Type (если сама переменная — класс), Literal, Final, …
Новости о Python 3.10
(Оказалось, что не новости)
Отложенная аннотация: pep-0563 (обещали в 3.10, но ещё не включили)
Аннотации могут быть строковыми (и вообще какими угодно)
Типы хинтов можно вычислить с помощью typing.get_type_hints(), однако это прямой вызов eval() ☹: {{{##!highlight pycon
>>> import inspect >>> import typing >>> class C: ... a: "D" >>> class D: pass >>> inspect.get_annotations(C) {'a': 'D'} >>> typing.get_type_hints(C) {'a': <class 'main.D'>} }}}
Развесистая статья на Хабре (⩽ Python3.8, однако ☺, см pep-0585)
dataclass — типизированные структуры
Важно: в Python есть поддержка аннотаций, но практически нет их использования (разве что в dataclasses). В язык не входит, делайте сами.
MyPy
Зачем аннотации?
- Дисциплина программирования
- большие, сверхбольшие и «долгие» проекты
Потенциально возможные проверки
- Прагматика, включенная в синтаксис языка
- Преобразование 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
1 #!/usr/bin/env python3
2 import time
3
4
5 def fb(x: int, y: int) -> tuple[int, int]:
6 return y, x + y
7
8
9 def test() -> float:
10 x: int = 0
11 y: int = 1
12 t: float = time.time()
13 for i in range(1000000):
14 x = 0
15 y = 1
16 for j in range(100):
17 x, y = fb(x, y)
18 return time.time() - t
Сравнение производительности:
1 $ mypyc speed.py
2 running build_ext
3 building 'speed' extension
4 creating build/temp.linux-x86_64-3.9
5 creating build/temp.linux-x86_64-3.9/build
6 x86_64-alt-linux-gcc -Wno-unused-result -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -g -fwrapv -O3 -Wall -pipe -frecord-gcc-switches -Wall -g -O3 -flto=auto -ffat-lto-objects -pipe -frecord-gcc-switches -Wall -g -O3 -flto=auto -ffat-lto-objects -fPIC -I/home/george/venv/_mypy/lib64/python3/site-packages/mypyc/lib-rt -I/home/george/venv/_mypy/include -I/usr/include/python3.9 -c build/__native.c -o build/temp.linux-x86_64-3.9/build/__native.o -O3 -Werror -Wno-unused-function -Wno-unused-label -Wno-unreachable-code -Wno-unused-variable -Wno-unused-command-line-argument -Wno-unknown-warning-option -Wno-unused-but-set-variable
7 x86_64-alt-linux-gcc -shared build/temp.linux-x86_64-3.9/build/__native.o -L/usr/lib64 -o /home/george/venv/_mypy/speed.cpython-39.so
8 $ mv speed.py speed_.py
9 $ ls
10 build ex1.py __pycache__ speed.cpython-39.so speed_.py
11 $ python3 -c "import speed_; print(speed_.test())"
12 9.72582745552063
13 $ python3 -c "import speed; print(speed.test())"
14 2.2559256553649902
15
Д/З
Задача сложная, присутствует исследование документации!
- Прочитать про:
- Метаклассы
Модуль inspect (вот тут исследование)
- Аннотации
EJudge: MyMypy 'Типизация вручную'
Написать метакласс checked, при использовании которого в порождаемом классе проверяются соответствия типов всех аннотированных параметров и возвращаемых значений всех методов. В случае несовпадения инициируется TypeError: Type mismatch: <параметр>
- Значения параметров по умолчанию не проверяются (для простоты)
- Поля не проверяются, только методы
Предполагается, что должна быть верна проверка isinstance(объект, тип), в противном случае соответствие считается неуспешным
При вызове (по крайней мере, в тестах) не используются конструкции *args и **kwargs
Параметры проверяются (если они аннотированы, конечно) строго в следующем порядке
- Позиционные (переданные в кортеже и распакованные) параметры, по порядку
- Явно заданные именные (переданные в словаре и распакованные) параметры в порядке вызова метода
Нераспакованные позиционные параметры, полученные через *args. В этом случае в строке исключения пишется args (имя, которое при описании функции принимало запакованные позиционные параметры)
Нераспакованные именные параметры, полученные через **kwargs
Возвращаемое значение (имя параметра "return", как в аннотации)
1 class E(metaclass=checked): 2 def __init__(self, var: int): 3 self.var = var if var%2 else str(var) 4 5 def mix(self, val: int, opt) -> int: 6 return self.var*val + opt 7 8 def al(self, c: int, d:int=1, *e:int, f:int=1, **g:int): 9 return self.var*d 10 11 e1, e2 = E(1), E(2) 12 code = """ 13 e1.mix("q", "q") 14 e1.mix(2, 3) 15 e2.mix(2, "3") 16 e1.al("q") 17 e1.al(1, 2, 3, 4, 5, 6, foo=7, bar=8) 18 e2.al(1, 2, 3, 4, 5, 6, foo=7, bar=8) 19 e1.al("E", 2, 3, 4, 5, 6, foo=7, bar=8) 20 e1.al(1, "E", 3, 4, 5, 6, foo=7, bar=8) 21 e1.al(1, 2, "E", 4, 5, 6, foo=7, bar=8) 22 e1.al(1, 2, 3, "E", 5, 6, foo="7", bar=8) 23 e1.al(1, f="E", d=1) 24 e1.al(1, f=1, d="E") 25 e1.al(1, f="E", d="1") 26 e1.al(1, d="E", f="1") 27 e1.al(1, e="E") 28 e1.al(1, g="E") 29 """ 30 31 for c in code.strip().split("\n"): 32 try: 33 res = eval(c) 34 except TypeError as E: 35 res = E 36 print(f"Run: {c}\nGot: {res}")
Run: e1.mix("q", "q") Got: Type mismatch: val Run: e1.mix(2, 3) Got: 5 Run: e2.mix(2, "3") Got: Type mismatch: return Run: e1.al("q") Got: Type mismatch: c Run: e1.al(1, 2, 3, 4, 5, 6, foo=7, bar=8) Got: 2 Run: e2.al(1, 2, 3, 4, 5, 6, foo=7, bar=8) Got: 22 Run: e1.al("E", 2, 3, 4, 5, 6, foo=7, bar=8) Got: Type mismatch: c Run: e1.al(1, "E", 3, 4, 5, 6, foo=7, bar=8) Got: Type mismatch: d Run: e1.al(1, 2, "E", 4, 5, 6, foo=7, bar=8) Got: Type mismatch: e Run: e1.al(1, 2, 3, "E", 5, 6, foo="7", bar=8) Got: Type mismatch: e Run: e1.al(1, f="E", d=1) Got: Type mismatch: f Run: e1.al(1, f=1, d="E") Got: Type mismatch: d Run: e1.al(1, f="E", d="1") Got: Type mismatch: f Run: e1.al(1, d="E", f="1") Got: Type mismatch: d Run: e1.al(1, e="E") Got: Type mismatch: e Run: e1.al(1, g="E") Got: Type mismatch: g