Внимание. Интерпретатор Python не проверяет и не принимает во внимание аннотации типов функций и переменных. Их могут использовать сторонние инструменты, такие как средства проверки типов, IDE, линтеры и т. д.
Модуль typing
обеспечивает поддержку выполнения аннотации типов. Наиболее фундаментальная поддержка состоит из типов typing.Any
, typing.Union
, typing.Tuple
, typing.Callable
, typing.TypeVar
и typing.Generic
.
Функция ниже принимает и возвращает строку и аннотируется следующим образом:
def greeting(name: str) -> str: return 'Hello ' + name
В функции greeting()
ожидается, что имя аргумента будет иметь тип str
и возвращаемый тип ожидается str
. Подтипы принимаются в качестве аргументов типов. Например переменная списка lst
, состоящий из значений int
будет аннотироваться как lst: list[int]
.
Примечание. Модуль typing
определяет несколько типов, которые являются подклассами уже существующих классов стандартной библиотеки, и которые также расширяют typing.Generic
для поддержки типов переменных внутри []
скобок. С версии Python 3.9, классы стандартной библиотеки были расширены для поддержки синтаксиса []
и эти типы стали избыточными.
Избыточные типы устарели в Python 3.9, но интерпретатор не будет выдавать предупреждений DEPRECATED
. Ожидается, что средства проверки типов будут отмечать устаревшие типы, когда проверяемый код нацелен на версию Python 3.9 или новее.
Устаревшие типы будут удалены из модуля typing
в первой версии Python, выпущенной через 5 лет после выпуска Python 3.9.0.
typing.NewType
;typing.Any
;typing
.Начиная с версии Python 3.12, псевдоним типа определяется с помощью оператора type
(добавлен в Python 3.12), который создает экземпляр TypeAliasType
. В этом примере Vector
и list[float]
будут обрабатываться средствами проверки статического типа одинаково:
# синтаксис ДО Python 3.12 # Vector = list[float] # синтаксис начиная с Python 3.12 type Vector = list[float] def scale(scalar: float, vector: Vector) -> Vector: return [scalar * num for num in vector] # Тип будет проверяться; # список значений `float` квалифицируется как вектор. new_vector = scale(2.0, [1.0, -4.2, 5.4])
Псевдонимы типов полезны для упрощения сигнатур сложных типов. Например:
from collections.abc import Sequence # синтаксис ДО Python 3.12 # ConnectionOptions = dict[str, str] # Address = tuple[str, int] # Server = tuple[Address, ConnectionOptions] # синтаксис начиная с Python 3.12 type ConnectionOptions = dict[str, str] type Address = tuple[str, int] type Server = tuple[Address, ConnectionOptions] def broadcast_message(message: str, servers: Sequence[Server]) -> None: ... # Средство проверки статического типа будет рассматривать # подпись предыдущего типа как точно эквивалентную этой. def broadcast_message( message: str, servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None:
Оператор
type
является новым в Python 3.12.
Для обратной совместимости псевдонимы типов также можно создавать с помощью простого присваивания:
Vector = list[float]
Или пометить typing.TypeAlias
, чтобы было ясно, что это псевдоним типа, а не обычное присвоение переменной:
from typing import TypeAlias Vector: TypeAlias = list[float]
typing.NewType
.Используйте вспомогательный класс typing.NewType()
для создания отдельных типов:
from typing import NewType UserId = NewType('UserId', int) some_id = UserId(524313)
Средство проверки статического типа будет рассматривать новый тип, как если бы он был подклассом исходного типа. Такое поведение полезно для выявления логических ошибок:
def get_user_name(user_id: UserId) -> str: ... # Тип будет проверяться user_a = get_user_name(UserId(42351)) # тип не проверяется; `int` не является UserId user_b = get_user_name(-1)
По-прежнему можно выполнять все операции с int
с переменной типа UserId
, но результат всегда будет иметь тип int
. Это позволяет передать UserId
везде, где можно ожидать int
, но предотвратит случайное создание UserId
недопустимым способом:
# 'output' имеет тип 'int', а не 'UserId' output = UserId(23413) + UserId(54341)
Обратите внимание, что эти проверки выполняются только средством проверки типов. Во время выполнения оператор Derived= NewType('Derived', Base)
сделает Derived
функцией, которая немедленно возвращает любой переданный ей параметр. Это означает, что выражение Derived(some_value)
не создает новый класс и не вводит никаких накладных расходов, помимо обычных вызовов функции.
Точнее, выражение some_value is Derived(some_value)
всегда истинно во время выполнения.
Это также означает, что невозможно создать подтип Derived
, т. к. во время выполнения это функция идентификации, а не фактический тип:
from typing import NewType UserId = NewType('UserId', int) # Не работает во время выполнения и не проверяет тип class AdminUserId(UserId): pass
Однако можно создать typing.NewType()
на основе производного NewType
:
from typing import NewType UserId = NewType('UserId', int) ProUserId = NewType('ProUserId', UserId)
и проверка типов для ProUserId
будет работать так, как ожидалось.
Примечание. Напомним, что использование псевдонима типа объявляет, что два типа эквивалентны друг другу. Выполнение
Alias = Original
заставит средство проверки статического типа обрабатывать псевдоним как полностью эквивалентный оригиналу во всех случаях. Это полезно, когда необходимо упростить сигнатуры сложных типов.Напротив,
typing.NewType
объявляет один тип подтипом другого. ВыполнениеDerived = NewType('Derived', Original)
заставит средство проверки аннотации типов рассматриватьDerived
как подклассOriginal
, это означает, что значение типаOriginal
не может использоваться в местах, где ожидается значение типаDerived
. Такое поведение полезно, когда необходимо предотвратить логические ошибки с минимальными затратами времени выполнения.Изменено в Python 3.10:
NewType
теперь является классом, а не функцией. При вызовеNewType
вместо обычной функции возникают некоторые дополнительные затраты времени выполнения. В Python 3.11.0 эти затраты будут снижены.Изменено в версии 3.11: Производительность вызова
NewType
восстановлена до уровня Python 3.9.
Функции или другие вызываемые объекты можно аннотировать с помощью Collections.abc.Callable
или typing.Callable
. Callable[[int], str]
означает функцию, которая принимает один параметр типа int
и возвращает строку str
.
Например:
from collections.abc import Callable, Awaitable def feeder(get_next_item: Callable[[], str]) -> None: ... # тело функции def async_query(on_success: Callable[[int], None], on_error: Callable[[int, Exception], None]) -> None: ... # тело функции async def on_update(value: str) -> None: ... # тело функции callback: Callable[[str], Awaitable[None]] = on_update
Синтаксис подписки Callable()
всегда должен использоваться ровно с двумя значениями: списком аргументов и типом возвращаемого значения. Список аргументов должен быть списком типов, ParamSpec
, Concatenate
или многоточием. Возвращаемый тип должен быть одного типа.
Если в качестве списка аргументов указано многоточие ...
, это указывает на то, что вызываемый объект с любым произвольным списком параметров был бы приемлемым:
def concat(x: str, y: str) -> str: return x + y x: Callable[..., str] x = str # OK x = concat # Also OK
Callable()
не может выражать сложные сигнатуры, такие как функции, которые принимают переменное количество аргументов, overloaded functions или функции, которые имеют только ключевые аргументы. Однако эти сигнатуры могут быть выражены путем определения класса typing.Protocol
с методом __call__()
:
from collections.abc import Iterable from typing import Protocol class Combiner(Protocol): def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ... def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes: for item in data: ... def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]: ... def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]: ... batch_proc([], good_cb) # OK # Ошибка! Аргумент 2 имеет несовместимый тип из-за # другого имени и вида в обратном вызове batch_proc([], bad_cb)
Вызываемые объекты, которые принимают другие вызываемые объекты в качестве аргументов, могут указывать на то, что их типы параметров зависят друг от друга с помощью ParamSpec
. Кроме того, если этот вызываемый объект добавляет или удаляет аргументы из других вызываемых объектов, то может использоваться оператор typing.Concatenate
. Они принимают форму Callable[ParamSpecVariable, ReturnType]
и Callable[Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable], ReturnType]
соответственно.
Изменено в версии Python 3.10:
Callable
теперь поддерживаетParamSpec
иConcatenate
.Также смотрите документацию для
typing.ParamSpec
иtyping.Concatenate
]typing.Concatenate, в которой приведены примеры использования вCallable
.
Так как информация о типах объектов, хранящихся в контейнерах, не может быть статически представлена универсальным способом, для обозначения ожидаемых типов элементов контейнера, были расширены абстрактные базовые классы.
from collections.abc import Mapping, Sequence class Employee: ... # `Sequence[Employee]` указывает, что все элементы в последовательности # должны быть экземплярами `Employee`. # `Mapping[str, str]` указывает, что все ключи и все значения в сопоставлении # должны быть строками. def notify_by_email(employees: Sequence[Employee], overrides: Mapping[str, str]) -> None: ...
Универсальные функции и классы могут быть аннотированы с помощью синтаксиса списка параметров типа:
from collections.abc import Sequence # Функция является универсальной для TypeVar "T" def first[T](l: Sequence[T]) -> T: return l[0]
Или с помощью фабрики TypeVar
напрямую:
from collections.abc import Sequence from typing import TypeVar # Объявляем переменную типа "U" U = TypeVar('U') # Функция является универсальной по сравнению с TypeVar "U", def second(l: Sequence[U]) -> U: return l[1]
В Python 3.12 пользовательский класс можно определить как универсальный класс.
from logging import Logger class LoggedVar[T]: def __init__(self, value: T, name: str, logger: Logger) -> None: self.name = name self.logger = logger self.value = value def set(self, new: T) -> None: self.log('Set ' + repr(self.value)) self.value = new def get(self) -> T: self.log('Get ' + repr(self.value)) return self.value def log(self, message: str) -> None: self.logger.info('%s: %s', self.name, message)
Этот синтаксис указывает, что класс LoggedVar
аннотирован вокруг одного type variable T
. Это также делает T
допустимым типом в теле класса.
Для совместимости с Python < 3.11 также возможно явное наследование от Generic
для указания универсального класса:
from typing import TypeVar, Generic from logging import Logger T = TypeVar('T') class LoggedVar(Generic[T]): def __init__(self, value: T, name: str, logger: Logger) -> None: self.name = name self.logger = logger self.value = value def set(self, new: T) -> None: self.log('Set ' + repr(self.value)) self.value = new def get(self) -> T: self.log('Get ' + repr(self.value)) return self.value def log(self, message: str) -> None: self.logger.info('%s: %s', self.name, message)
Generic[T]
как базовый класс определяет, что класс LoggedVar
принимает единственный параметр типа T
. Это также делает T
действительным как тип в теле класса.
Базовый класс typing.Generic
определяет __class_getitem__()
, так что LoggedVar[t]
действителен как тип:
from collections.abc import Iterable def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None: for var in vars: var.set(0)
Универсальный тип может иметь любое количество переменных типа. Все разновидности TypeVar
допустимы в качестве параметров для универсального типа:
from typing import TypeVar, Generic, Sequence class WeirdTrio[T, B: Sequence[bytes], S: (int, str)]: ... OldT = TypeVar('OldT', contravariant=True) OldB = TypeVar('OldB', bound=Sequence[bytes], covariant=True) OldS = TypeVar('OldS', int, str) class OldWeirdTrio(Generic[OldT, OldB, OldS]): ...
Все аргументы переменной типа для typing.Generic
должны быть разными. Таким образом, следующее неверно:
from typing import TypeVar, Generic ... T = TypeVar('T') class Pair(Generic[T, T]): # INVALID ...
Вы можете использовать множественное наследование с typing.Generic
:
from collections.abc import Sized from typing import TypeVar, Generic T = TypeVar('T') class LinkedList(Sized, Generic[T]): ...
При наследовании от универсальных классов можно исправить некоторые переменные типа:
from collections.abc import Mapping from typing import TypeVar T = TypeVar('T') class MyDict(Mapping[str, T]): ...
В этом случае MyDict
имеет единственный параметр T
.
Использование универсального класса без указания параметров типа предполагает typing.Any
для каждой позиции. В следующем примере MyIterable
не является универсальным, а неявно наследуется от Iterable[Any]
:
from collections.abc import Iterable # Такой же как `Iterable[Any]` class MyIterable(Iterable):
В Python 3.12 поддерживаются определенные пользователем псевдонимы универсального типа. Примеры:
from collections.abc import Iterable type Response[S] = Iterable[S] | int # Возвращаемый тип здесь такой же, как Iterable[str] | int def response(query: str) -> Response[str]: ... type Vec[T] = Iterable[tuple[T, T]] # То же, что и Iterable[tuple[T, T]] def inproduct[T: (int, float, complex)](v: Vec[T]) -> T: return sum(x*y for x, y in v)
Для обратной совместимости псевдонимы универсальных типов также можно создавать с помощью простого присваивания:
from collections.abc import Iterable from typing import TypeVar S = TypeVar("S") Response = Iterable[S] | int
Изменено в Python 3.7:
typing.Generic
больше не имеет собственного метакласса.Изменено в Python 3.12: Синтаксическая поддержка универсальных шаблонов и псевдонимов типов появилась в Python 3.12. Раньше универсальные классы должны были явно наследоваться от
Generic
или содержать переменную типа в одной из своих баз.
Определяемые пользователем универсальные типы для аннотирования параметров также поддерживаются с помощью переменных спецификации параметров в форме [**P]
. Поведение согласуется с описанными выше переменными типа, поскольку переменные спецификации параметров обрабатываются модулем ввода как специализированная переменная типа. Единственным исключением из этого является то, что список типов может быть использован для замены параметра SPEC
:
# T - это переменная типа; # P - это ParamSpec >>> class Z[T, **P]: ... ... >>> Z[int, [dict, float]] # __main__.Z[int, [dict, float]]
Классы, универсальные для ParamSpec
, также можно создавать с использованием явного наследования от Generic
. В этом случае **
не используется:
>>> from typing import Generic, ParamSpec, TypeVar >>> T = TypeVar('T') >>> P = ParamSpec('P') >>> class Z(Generic[T, P]): ... ... >>> Z[int, [dict, float]] # __main__.Z[int, (<class 'dict'>, <class 'float'>)]
Еще одно различие между TypeVar
и ParamSpec
заключается в том, что универсальный вариант только с одной переменной спецификации параметра будет принимать списки параметров в формах X[[Type1, Type2, ...]]
, а также X[Type1, Type2, ...]
по эстетическим соображениям. Внутри последние преобразуются в первые и, таким образом, эквивалентны:
>>> class X[**P]: ... ... >>> X[int, str] # __main__.X[[int, str]] >>> X[[int, str]] # __main__.X[[int, str]]
Обратите внимание, что универсальные типы с typing.ParamSpec
, в некоторых случаях, могут не иметь правильных __parameters__
после подстановки, потому что они предназначены в первую очередь для проверки статического типа.
Изменено в Python 3.10:
Generic
теперь можно параметризовать с помощью выраженийParamSpec
.
Определенный пользователем универсальный класс может иметь ABC
в качестве базовых классов без конфликта метаклассов. Универсальные метаклассы не поддерживаются. Результат параметризации универсальных шаблонов кэшируется, и большинство типов в модуле типизации являются хэшируемыми и сопоставимыми по равенству.
typing.Any
.Особый тип аннотации typing.Any
. Средство проверки статического типа будет рассматривать каждый тип как совместимый с Any
и Any
как совместимый с каждым типом.
Это означает, что можно выполнить любую операцию или вызов метода для значения типа Any
и присвоить его любой переменной:
from typing import Any a = None # type: Any a = [] # OK a = 2 # OK s = '' # type: str s = a # OK def foo(item: Any) -> int: # Проверка типов; 'item' может быть # любого типа, и этот тип может # иметь метод 'bar' item.bar() ...
Обратите внимание, что при присвоении значения типа Any
более точному типу, проверка типов выполняться не будет. Например, средство проверки аннотации не сообщило об ошибке при присвоении a
параметру s
, даже если s
был объявлен как имеющий тип str
и получил значение int
во время выполнения!
Кроме того, все функции без возвращаемого типа или типов параметров неявно по умолчанию будут использовать Any:
def legacy_parser(text): ... return data # Статическая проверка типов будет # рассматривать вышеприведенное # как имеющее ту же сигнатуру, что и: def legacy_parser(text: Any) -> Any: ... return data
Такое поведение позволяет использовать typing.Any
в качестве аварийного выхода, когда необходимо смешивать динамически и статически типизированный код.
Сравните поведение typing.Any
с поведением object
: Подобно Any
, каждый тип является подтипом object
. Однако, в отличие от Any
, обратное неверно: object
не является подтипом любого другого типа.
Это означает, что, когда типом значения является объект, то средство проверки типов отклоняет почти все операции с ним, и присвоение его переменной (или использование в качестве возвращаемого значения) более специализированного типа является ошибкой типа. Например:
def hash_a(item: object) -> int: # Не проходит; у `object` нет `magic` метода. item.magic() ... def hash_b(item: Any) -> int: # Проверка типа прошла успешно item.magic() ... # Проверка прошла, поскольку `ints` и # `strs` являются подклассами объекта hash_a(42) hash_a("foo") # Проверка прошла, т.к. `Any` # совместим со всеми типами hash_b(42) hash_b("foo")
Используйте object
, для указания значения любого типа безопасным способом, а typing.Any
, для динамически типизируемых значений.
Первоначально определи систему статических типов Python, как использование номинальных подтипов. Это означает, что класс A
разрешен там, где ожидается класс B
тогда и только тогда, когда A
является подклассом B
.
Это требование ранее также применялось к абстрактным базовым классам, таким как Iterable
. Проблема с этим подходом заключается в том, что класс должен быть явно помечен для их поддержки, что не является питоническим и отличается от того, что обычно делают в идиоматическом динамически типизированном коде Python. Например:
from collections.abc import Sized, Iterable, Iterator class Bucket(Sized, Iterable[int]): ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[int]: ...
Однако пользователи могут писать приведенный выше код без явных базовых классов в определении класса, что позволяет средствам проверки статических типов неявно рассматривать Bucket
как подтип Sized
и Iterable[int]
. Это называется структурным подтипом или статической утиной типизацией:
from collections.abc import Iterator, Iterable # Примечание: нет базовых классов class Bucket: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[int]: ... def collect(items: Iterable[int]) -> int: ... result = collect(Bucket()) # Проходит проверку типа
Более того, создавая подклассы для специального класса Protocol
, пользователь может определять новые настраиваемые протоколы, чтобы в полной мере насладиться структурным подтипом.