В питоне сложилась довольно сложная ситуация с типизацией. В отличии от многих других языков, у нас нет специального "мини-языка типов". Что сильно упрощает работу с типизацией для авторов языка. Ведь им не нужно поддерживать поведение объектов типизации в рантайме.
А у нас есть только старый добрый питон и его объекты. Да, в питоне – действительно всё объект. Даже типизация. Оттого, любой код, который вы пишите в аннотациях – должен корректно работать в рантайме. Ведь у нас есть потребители такого, например:
pydantic.Скажем, у нас есть TypeAliasType объект с одной типовой переменной. Мы хотим её явно указать:
type StrDict[_V] = dict[str, _V]
numbers: StrDict[int] = {'one': 1}
Тут все довольно просто в рантайме:
StrDict[int] вызовет __getitem__ вот тут. Кстати, там Cшный код, конечно же. И нам просто вернется нужный GenericAlias объект с нужными __args__:
>>> type StrDict[_V] = dict[str, _V]
>>> StrDict[int], type(StrDict[int]), StrDict[int].__args__
(StrDict[int], <class 'types.GenericAlias'>, (<class 'int'>,))
Мы видим, что замена
_V на int работает! Пока что 🌚А как физически происходит замена переменной?
Начнем с того, что в Python есть 3 TypeVar-like объекта:
TypeVar, TypeVarTuple, ParamSpec. У каждого из них есть свои правила, как можно их заменять (в некоторых контекстах):-
TypeVar можно заменять на одно другое типовое значение-
TypeVarTuple можно заменять на любое количество типовых значений-
ParamSpec можно заменять на Concatenate, ... и список типовПример ошибки замены, исходник:
>>> from collections.abc import Callable
>>> from typing import ParamSpec, TypeVar
>>> P = ParamSpec('P')
>>> R = TypeVar('R')
>>> Callable[P, R][0, int]
Traceback (most recent call last):
TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got <class 'int'>
Пытаемся заменить
P на 0, что не является корректной заменой.Но как фактически проверить, что замена корректная? И вот тут входит в дело
__typing_subst__.Он определен у всех типов TypeVar-like. Мы посмотрим на ParamSpec. Все, конечно же, написано на дикой смеси C и Python. И Cшная реализация
ParamSpec.__typing_subst__ вызывает typing._paramspec_subst. Почему так? В 3.12 все переписали на C в спешке. Но некоторые части были слишком сложны, их оставили на питоне.Аналогично:
>>> P.__typing_subst__([])
()
>>> P.__typing_subst__([int, str])
(<class 'int'>, <class 'str'>)
>>> P.__typing_subst__(...)
Ellipsis
>>> P.__typing_subst__(0)
Traceback (most recent call last):
TypeError: Expected a list of types, an ellipsis, ParamSpec, or Concatenate. Got 0
Подготовка к замене
Но, все еще чуть-чуть сложнее. Потому что перед тем как что-то заменить, нам нужно подготовиться к замене.
Есть второй магический метод, исходники:
__typing_prepare_subst__. Он нужен, чтобы собрать аргументы для замены. Потому что у нас, например, могут быть неявные аргументы с default. Проверим на TypeVarTuple:
>>> class Custom[T1, *Ts=(int, int)]: ...
...
>>> Custom[str]
__main__.Custom[str, [int, int]]
>>> Custom[str, str]
__main__.Custom[str, str]
Ну или напрямую:
>>> Custom.__type_params__[1].__typing_prepare_subst__(*Custom.__type_params__[1], ())
([(<class 'int'>, <class 'int'>)],)
>>> Custom.__type_params__[1].__typing_prepare_subst__(*Custom.__type_params__[1], (str,))
((<class 'str'>,),)
Здесь в первом случае
[int, int] нам как раз добавили в __args__ через __typing_prepare_subst__ вот тут.Вот такая машинерия выполняется каждый раз, когда нам нужен
Generic с параметрами. Потому с 3.14 все аннотации будут ленивыми по-умолчанию. А __annotate__ будет выполняться только тогда, когда аннотации будут запрашивать реально для рантайма.Обсуждение: задумывались ли вы о работе аннотаций типов в рантайме? Сталкивались ли с проблемами в данной сфере? Тормозило?
| Поддержать | YouTube | GitHub | Чат |