Аннотации и статическая типизация
Аннотации
Базовая статья: О дисциплине использования аннотаций
(немного копипасты из ../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.14 будут pep-0649, аннотации-дескрипторы, т. е. вычисляемые по факту обращения, а в 3.15, скорее всего, строковые аннотации задепрекейтят
Аннотации настолько отвязаны от реализации, что, например, получить доступ к собственным аннотациям из функции, например, довольно трудно, и получается «хрупкий» код (с классами гораздо проще)
Составные и нечёткие типы
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(numerator=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() as a: 12 print(f"Unknown number {a}") 13 case _: 14 print("NaN")
Алиасы (практически typedef) и NewType (категоризация)
Классы как переменные (Python 3.13: typing.TypeVar
В том числе конструкции вида type[SubclassA | SubclassB]
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? ) и дженерики переменной длины
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.12.5 / ALT):
- В Python 3.11 на этом же компьютере результаты были 178 msec и 13.2 msec соответственно
Д/З
- Прочитать про
Аннотации, модули collections.abc и typing
EJudge: CookedStruct 'Класс с преобразованием'
(фактически это упражнение). Написать класс Cooked, который будет использовать аннотации к полям этого класса в качестве функций преобразования значений во время присваивания их полям (см. пример). Исключения, которые могут возникнуть во время присваивания, не обрабатывать. Предусмотреть также преобразование в строку, которое должно возвращать перечень аннотированных полей (в порядке их появления в словаре аннотаций и если они присутствуют в объекте) в формате ":имя=значение …:"; если таких полей нет, выводится "::".
:a=1234 b=1:
EJudge: DodgsonDet 'Определитель'
Ввести квадратную целочисленную матрицу построчно и посчитать её определитель (например, методом конденсации Доджсона). Размер матрицы (1<N⩽20) определяется длиной её нулевой строки. Описание предлагаемого метода в подсказках. Пользоваться numpy.linalg.det(arr) нельзя ☺.
Дополнительно текст программы будет проверяться mypyc --strict, ошибок быть не должно
8, 8, 5, 6, 3 1, 4, 4, 9, 0 9, 6, 7, 7, 3 4, 1, 0, 1, 4 6, 7, 9, 7, 3
2784
EJudge: StrictSort 'Строгая сортировка'
(эту задачу надо сдавать в EJudge, но некоторые свойства решения там пока проверить нельзя) Написать:
функцию defkey(element: Comparable) -> Comparable, которая вычисляет значение ключа сортировки по следующему принципу:
Если element имеет длину, то ключ — это длина
Иначе ключ — это сам element (это допустимо в силу его типа)
функцию strictsort(seq: Sortable, key: Callable[[Comparable], Comparable] = defkey) -> Sortable:, которая сортирует элементы изменяемой последовательности (в самой последовательности) и возвращает эту последовательность
Должно быть определено два дополнительных типа:
Comparable — тип, в котором есть операция сравнения (для простоты — на меньше)
Sortable — изменяемая индексируемая последовательность элементов типа Comparable
В результате приведённый пример должен проходить mypy --strict, а любая закомментированная строка из примера — вызывать ошибку проверки/компиляции (строго до runtime).
1 c: Sortable = [6, 1, 4, 2, 7, 2, 8, 3] 2 print(*strictsort(c)) 3 print(*strictsort([6., 1., 4., 2., 7., 2., 8., 3.])) 4 print(*strictsort(["234", "sdf23452345234gg", "45645674567", "ASDASD"])) 5 print(*strictsort([(1, 2, 4, 9), (2, 7, 4, 6, 8), (1,), (8, 9, 23), (7, 2, 1)])) 6 print(defkey(9), defkey("999")) 7 # c: Sortable = [7j, 2j, 3j] 8 # defkey(iter("123")) 9 # print(*strictsort({1: 2, 3: 4}))
1 2 2 3 4 6 7 8 1.0 2.0 2.0 3.0 4.0 6.0 7.0 8.0 234 ASDASD 45645674567 sdf23452345234gg (1,) (8, 9, 23) (7, 2, 1) (1, 2, 4, 9) (2, 7, 4, 6, 8) 9 3