Главная прелесть питона — duck typing. Про него мы много раз говорили, там ноль проблем сдомашками (30 строк на домашку) итд
Но
Все ошибки рантаймовые, с ними трудно работать. Чем больше программа, тем больше процент того, что мы не то передадим, не так обратимся итд
Часть прагматики растворяется в коде, с этим проблема, хорошо бы знать, что не надо передавать в функции
Аннотации — такие описания типов, с помощью которых можно коммуницировать с пользователем
>>> import annotationlib
>>> class C:
... A: int = 2
... B: float
... def __init__(self, param: int = None, signed: bool = True) -> None:
... if param is not None:
... self.A = param if signed else abs(param)
... def mult(self, mlt: int) -> str:
... return self.A * mlt
...
>>> c = C(100)
>>> s = C("QWE")
>>> c.mult(3)
300
>>> c.mult("q")
'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'
>>> s.mult(3)
'QWEQWEQWE'
>>>
>>> C.__annotations__
{'A': <class 'int'>, 'B': <class 'float'>}
>>> C.A
2
>>> c.A
100
>>> s.A
'QWE'
>>> C.B
Traceback (most recent call last):
File "<python-input-13>", line 1, in <module>
C.B
AttributeError: type object 'C' has no attribute 'B'
>>>
Аннотацию можно получить и по-другому
>>> C.__annotate_func__(annotationlib.Format.VALUE)
{'A': <class 'int'>, 'B': <class 'float'>}
>>>
>>> annotationlib.get_annotations(C)
{'A': <class 'int'>, 'B': <class 'float'>}
>>> annotationlib.get_annotations(C, format=annotationlib.Format.STRING)
{'A': 'int', 'B': 'float'}
>>> annotationlib.get_annotations(c.__init__)
{'param': <class 'int'>, 'signed': <class 'bool'>, 'return': None}
>>> annotationlib.get_annotations(s.mult)
{'mlt': <class 'int'>, 'return': <class 'str'>}
>>>
Аннотации — это только подсказки и не более. Но в них самые настоящие типы, с ними можно работать
До 3.14 питона нельзя было делать аннотации на себя же. Теперь с помощью отложенного аннотирования так можно
>>> class C:
... next: C
...
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
class C:
next: C
File "<python-input-0>", line 2, in C
next: C
^
NameError: name 'C' is not defined
>>>
>>> class C:
... next: C
...
>>>
Можно даже так, только вывод формата аннотации будет особенный
>>> class C:
... next: D
...
>>> annotationlib.get_annotations(C)
Traceback (most recent call last):
File "<python-input-40>", line 1, in <module>
annotationlib.get_annotations(C)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^
File "/usr/lib64/python3.14/annotationlib.py", line 958, in get_annotations
ann = _get_dunder_annotations(obj)
File "/usr/lib64/python3.14/annotationlib.py", line 1133, in _get_dunder_annotations
ann = _BASE_GET_ANNOTATIONS(obj)
File "<python-input-39>", line 2, in __annotate__
next: D
^
NameError: name 'D' is not defined
>>> annotationlib.get_annotations(C, format=annotationlib.Format.STRING)
{'next': 'D'}
>>>
Есть формат отложенного вычисления
>>> res = annotationlib.get_annotations(C, format=annotationlib.Format.FORWARDREF)
>>> res
{'next': ForwardRef('D', is_class=True, owner=<class '__main__.C'>)}
>>> res.evaluate()
Traceback (most recent call last):
File "<python-input-46>", line 1, in <module>
res.evaluate()
^^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'evaluate'
>>> res['next'].evaluate()
Traceback (most recent call last):
File "<python-input-47>", line 1, in <module>
res['next'].evaluate()
~~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib64/python3.14/annotationlib.py", line 202, in evaluate
raise NameError(_NAME_ERROR_MSG.format(name=arg), name=arg)
NameError: name 'D' is not defined
>>> class D: pass
...
>>> res['next'].evaluate()
<class '__main__.D'>
>>>
Есть ещё отложенное вычисление типов через type
>>> type integer = int
>>> integer
integer
>>> type(integer)
<class 'typing.TypeAliasType'>
>>> integer.evaluate_value()
<class 'int'>
>>> annotationlib.call_evaluate_function(integer.evaluate_value, format=annotationlib.Format.STRING)
'int'
>>>
>>> type C = QQ
>>> C.evaluate_value()
Traceback (most recent call last):
File "<python-input-59>", line 1, in <module>
C.evaluate_value()
~~~~~~~~~~~~~~~~^^
File "<python-input-58>", line 1, in C
type C = QQ
^^
NameError: name 'QQ' is not defined
>>> annotationlib.call_evaluate_function(C.evaluate_value, format=annotationlib.Format.STRING)
'QQ'
>>> annotationlib.call_evaluate_function(C.evaluate_value, format=annotationlib.Format.FORWARDREF)
ForwardRef('QQ')
>>>
В целом, нигде не было сказано, что в аннотациях обязательно должны быть выражения, и оно должно на ходу вычисляться. Вот оно и не вычисляется.
Аннотации настолько оторваны от всего остального, что внутри функции почти нельзя читать её аннотацию.
Модуль typing и статическая типизация
У них есть отдельное сообщество даже. Отдельный ман итд.
Если коротко: есть модель статического описания через аннотации, под неё написана куча автопроверок итд. Зачем — см пункты из начала.
Интересное — составные типы
>>> def fun(var: list[int]) -> int:
... return sum(val)
...
>>> import annotationlib
>>> annotationlib.get_annotations(fun)
{'var': list[int], 'return': <class 'int'>}
>>> list[int]
list[int]
>>> type(list[int])
<class 'types.GenericAlias'>
>>> def fun(var: list[int|float]) -> int:
... return sum(val)
...
>>> tuple[int]
tuple[int]
>>>
>>> import typing
>>> typing.get_args(annotationlib.get_annotations(fun))
()
>>> annotationlib.get_annotations(fun)
{'var': list[int | float], 'return': <class 'int'>}
>>> annotationlib.get_annotations(fun)['val']
Traceback (most recent call last):
File "<python-input-76>", line 1, in <module>
annotationlib.get_annotations(fun)['val']
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
KeyError: 'val'
>>> annotationlib.get_annotations(fun)['var']
list[int | float]
>>> typing.get_args(annotationlib.get_annotations(fun)['var'])
(int | float,)
>>>
Все эти типы называются просто TypeAlias, потому что тянуть все эти гирлянды из типов — самоубийство
>>> type PATH = list[tuple[int, int]]
>>> from math import dist
>>> from itertools import pairwise
>>>
>>> def length(p: PATH) -> int:
... return sum(dist(a, b) for a, b in pairwise(p))
...
>>> length([(0,0), (3, 4), (-1,1)])
10.0
>>>
Абстрактные классы
Набор базовых классов, от которых, конечно, никто не наследовался, но с ними проходит isinstance
>>> from collections.abc import Iterable
>>> from collections.abc import Sequence
>>>
>>> isinstance([1, 2, 3], Iterable)
True
>>> isinstance([1, 2, 3], Sequence)
True
>>> isinstance((c for c in "QWE"), Sequence)
False
>>> isinstance((c for c in "QWE"), Iterable)
True
>>>
Тут просто сравнение методов
Есть отдельная библиотека numbers для проверки числовых типов
>>> import numbers
>>> def classify(num):
... match num:
... case numbers.Rational(numerator=n, denominator=d):
... print(f'{n}/{d}')
...
>>> classify(234)
234/1
>>> from fractions import Fraction
>>> classify(Fraction("1.23"))
123/100
>>>
>>> def classify(num):
... match num:
... case numbers.Rational(numerator=n, denominator=d):
... print(f'{n}/{d}')
... case numbers.Real(real=a):
... print(f"Real {a}")
...
>>> classify(.33)
Real 0.33
>>> def classify(num):
... match num:
... case numbers.Rational(numerator=n, denominator=d):
... print(f'{n}/{d}')
... case numbers.Real(real=a):
... print(f"Real {a}")
... case numbers.Number() as a:
... print(f"Unknown number {a}")
...
>>> classify(3 + 5j)
Unknown number (3+5j)
>>>
Дженерики
Псевдонимы мы посмотрели, а можно ещё писать шаблоны, как в плюсах
>>> def negate[T](value: T) -> T:
... return -value
...
>>> negate(123)
-123
>>>
MyPy
Он, по сути, вызывает все те инструменты, которые мы посмотрели и тыкает нас за несоблюдение оного