Аннотации и статическая типизация
Аннотации
(немного копипасты из ../12_MetaclassMatch
Базовая статья: О дисциплине использования аннотаций
Duck typing:
Экономия кода на описаниях и объявлениях типа
Экономия (несравненно бо́льшая) кода на всех этих ваших полиморфизмах
⇒ Компактный читаемый код, хорошее отношение семантика/синтаксис
⇒ Быстрое решение Д/З ☺
Однако:
Практически все ошибки — runtime, их сложно предсказать / локализовать
Много страданий от невнимательности (передал объект не того типа, и не заметил, пока не свалилось)
Вашей невнимательности не поможет даже хитрое IDE: оно тоже не знает о том, какого типа объекты правильные, какого — нет
- (соответственно, о полях вашего объекта тоже)
Часть прагматики растворяется в коде (например, вы написали строковую функцию, как об этом узнать?)
Большие и сильно разрозненные проекты всегда полны runtime-ошибок ⇒ чем их меньше, тем лучше
Поэтому нужны указания о типе полей классов, параметрах и возвращаемых значений функций/методов и т. п. — Аннотации (annotations)
- Пример аннотаций полей (переменных), параметров и возвращаемых значений
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) -> None: 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"{a.__annotations__=}") 18 print(f"{inspect.get_annotations(a.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) # Ошибка!
- Аннотации сами по себе не влияют на семантику кода
- …в т. ч. не занимаются проверкой типов
Аннотации заполняют словарь __annotations__ в соответствующем пространстве имён
…но не они заводят там имена
Рекомендуется обращаться к аннотации не напрямую, а с помощью inspect.get_annotations()
- Типы в аннотациях —
это настоящие типы
- …что не всегда возможно, например:
В действительности могут быть вообще чем угодно (например, строками)
- ⇒ Можно использовать для всяких альтернативных семантик
(не нашёл ничего лучшего, чем макросы в Python)
Можно включить, чтобы вообще всегда были строками (pep-0563):
Однако eval_str=True — это прямой вызов eval()
…как и typing.get_type_hints(C)
…так что пока pep-0563 не торопятся принимать
…и скорее всего в Python 3.13 будет pep-0649 (аннотации-дескрипторы, т. е. вычисляемые по факту обращения)
Аннотации настолько отвязаны от реализации, что, например, получить доступ к собственным аннотациям из функции очень трудно, и получается «хрупкий» код, см. прошлое Д/З.
Составные и нечёткие типы
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
Сборник Pep-ов, имеющих отношение к статической типизации
⇒ Это тема для целого курса.
Кратко:
Альтернативы вида number: int | float (но именно здесь лучше numbers.Real)
Пример numbers:
1 import numbers 2 3 def classify(num): 4 match num: 5 case numbers.Rational(numertor=a, denominator=b): 6 print(f"{a}/{b}") 7 case numbers.Real(real=a): 8 print(f"[{a}]") 9 case numbers.Complex(real=a, imag=b): 10 print(f"<{a}, {b}>") 11 case numbers.Number(a): 12 print(f"? {a}") 13 case _: 14 print("NaN")
Алиасы (практически typedef) и NewType (категоризация)
Callable/Awaitable
Базовые дженерики (из collections.abc)
Конструкции вида Sequence[int] вместо list[int] | tuple[int] | а что ещё?) и параметризованные)
Кстати, tuple[int] означает вообще не это, а кортеж из одного элемента
- Например, как узнать, что нечто — это последовательность:
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
Параметризированные дженерики (это уже почти C++ темплейты… или не почти, с учётом pep-695? ) и дженерики переменной длины
Классы как переменные
В том числе конструкции вида type[SubclassA | SubclassB]
Any, Self
Инструменты: NoReturn, Never, Union, Optional, Type (если сама переменная — класс), Literal, Final…
- …
- …
Пример: dataclasses — типизированные структуры, логика базируется на аннотациях
MyPy
Что показательно:
Статическая типизация в Python очень активно развивается, достаточно посмотреть сводный What's New и поискать там «type» или «typing».
три официальных блога Гвидо: The Mypy Blog, Neopythonic, The History of Python
Совпадение? Не думаю!™
Ещё раз: зачем аннотации?
- Дисциплина программирования
- большие, сверхбольшие и «долгие» проекты
- «индус-триальное программирование»©
- Прагматика, включенная в синтаксис языка
- открытая разработка и более полное информационное пространство
- Использование аннотация для задания дополнительной семантики
TODO примеры использования?
- Преобразование Python-кода в представления, требующие статической типизации
- …
http://www.mypy-lang.org: статическая типизация в Python by default (ну, почти… или совсем!)
- Проверка выражений с типизированными данными
В т. ч. проверка или не-проверка нетипизиварованных
- Пример:
- Он запускается! Но проверку на статическую типизацию не проходит:
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
На MyPy основано большинство дисциплин разработки и систем проверки кода в различных IDE.
Компиляция
Если все объекты полностью типизированы, у них имеется эквивалент в виде соответствующих структур PythonAPI. ЧСХ, у байт-кода тоже есть эквивалент в Python API
Таинственный mypyc
- Пока не рекомендуют использовать, но сами все свои модули им компилируют!
Пример для mypyc:
- Крайне неэффективная реализация чисел Фибоначчи
- Сравнение производительности:
- В Python 3.10 было «2 loops» — ещё один сомнительный тренд современного питона, повышение производительности
Д/З
- Прочитать про
Аннотации, модули collections.abc и typing
EJudge: MetaCheck 'Метакласс с проверкой'
Написать метакласс checker, с помощью которого при создании класса будет происходить два дополнительных действия:
Все аннотированные поля класса, имеющие заданные значения, будут проверяться на то, что значение соответствует аннотации-типу, и в случае несоответствия — инициироваться исключение TypeError
Для каждого неаннотированного поля, имеющего заданное значения числового типа, будет создаваться соответствующая аннотация
{'a': <class 'str'>, 'b': <class 'int'>} NOPE
EJudge: AnnoDoc 'Аннотации как документация'
Написать декоратор annodoc(), которым можно декорировать классы и функции. Декоратор должен просматривать аннотации объекта и выбирать из них только такие, у которых вместо типа в аннотации используется строка. Эти строки (если они есть) надо добавлять в документацию объекта так:
(всегда) В начало строки документации — имя: (где имя — это поле .__name__ объекта)
- ⇒ Если у объекта не было строки документации, она создаётся
В конец строки документации — Variable имя: аннотация-строка (поля класса, формальные параметры функции или метода)
В самый конец строки документации — Returns: аннотация-строка для возвращаемого значения
Аннотированный объект следует просмотреть рекурсивно, и для каждого аннотированного указанным способом атрибута изменить строку документации с помощью annodoc(). Гарантируется, что в тестах эта рекурсия конечна.
C: The class Variable const: constant Variable var: variable method: Variable x: parameter Returns: return value
EJudge: StrictBubble 'Типизированная сортировка пузырём'
(эту задачу надо сдавать в EJudge, но основные свойства решения там пока проверить нельзя) Написать функцию bubble(sequence: Sortable) -> Sortable, которая сортирует эелменты изменяемой последовательности и возвращает её в отсортированном виде, и type alias Sortable, задающий
- изменяемую последовательность,
- элементы которой поддерживают операцию сравнения.
В результате приведённый пример должен проходить mypy --strict, компилироваться mypyc и выполняться из полученной библиотеки, а любая закомментированная строка из примера — вызывать ошибку проверки/компиляции.
1 from typing import cast 2 3 c = [60, 66, 67, 64, 65, 68, 60, 63, 63, 67, 66, 66, 67, 64, 66, 68, 61, 67, 64, 65] 4 for s in ( bubble(cast(Sortable, c)), 5 bubble(list(map(float, c))), 6 bubble(list(map(str, c))), 7 bubble(list(map(list, map(str, c))))): 8 print(*s) 9 # bubble(list(map(complex, c))) 10 # bubble(tuple(map(float, c)))
60 60 61 63 63 64 64 64 65 65 66 66 66 66 67 67 67 67 68 68 60.0 60.0 61.0 63.0 63.0 64.0 64.0 64.0 65.0 65.0 66.0 66.0 66.0 66.0 67.0 67.0 67.0 67.0 68.0 68.0 60 60 61 63 63 64 64 64 65 65 66 66 66 66 67 67 67 67 68 68 ['6', '0'] ['6', '0'] ['6', '1'] ['6', '3'] ['6', '3'] ['6', '4'] ['6', '4'] ['6', '4'] ['6', '5'] ['6', '5'] ['6', '6'] ['6', '6'] ['6', '6'] ['6', '6'] ['6', '7'] ['6', '7'] ['6', '7'] ['6', '7'] ['6', '8'] ['6', '8']