
Первоначально python как язык с динамической типизацией не предполагал никакого явного описания типов используемых объектов и список возможных действий с объектом определялся в момент его инициализации (или изменения значения). С одной стороны это удобно для разработчика, поскольку не нужно беспокоиться о корректности определения типов (но в то же время осложняло работу IDE, поскольку механизмы автодополнения требовали анализа типа выражения в ближайшей инициализации). Но это также приводило к появлению странных ошибок (особенно при использовании глобальных переменных, что само по себе уже плохое решение) и стало особенно неприятным при появлении необходимости контроля типа значений в коллекциях и созданию функций с обобщенными типами. В Python 3.12 будет реализована поддержка нового синтаксиса для generic-типов (PEP 695) и в этой статье мы обсудим основные идеи этого подхода.
Прежде всего вспомним, как работают аннотации типов в Python. При определении переменной или аргумента функции можно дополнительно указать уточнение типа через двоеточие, а тип возвращаемого значения определяется через стрелку после списка аргументов. Например, для определения функции сложения двух целых чисел можно использовать такой код:
def sum(a:int, b:int) -> int:
c: int = a + b # локальная переменная с типом
return c
Подобные аннотации помогают IDE определять список допустимых операций и проверять корректность использования переменных и типа возвращаемого значения.
Для определения переменной, которая может не содержать значения (=None), можно использовать тип typing.Optional[int]
. Также для перечисления набора возможных типов допустимо использовать typing.Union[int,float]
. Также можно создавать коллекции указанного типа (например, список строк typing.List[str]
, словарь typing.Dict[str,str]
) . Однако, тип всегда должен быть указан явно и простым способом сделать класс для работы с произвольным типом данных так не получится. Например, мы хотим сделать собственную реализацию стека, который сможет хранить значения указанного при определении типа.
class Stack:
def __init__(self):
self._data = []
def push(self, item: str):
self._data.append(item)
def pop(self) -> str | None:
if self._data:
item = self._data.pop()
return item
else:
return None
Это будет успешно работать со строками, но как определить стек для произвольных значений? PEP646 определил возможность создавать обобщенные типы (typing.TypeVar
) и определение стека через них может быть выполнено следующим образом:
from typing import TypeVar, Generic, List, Optional
StackType = TypeVar('StackType')
class Stack(Generic[StackType]):
def __init__(self):
self._data: List[StackType] = []
def push(self, item: StackType):
self._data.append(item)
def pop(self) -> Optional[StackType]:
if self._data:
return self._data.pop()
else:
return None
stack = Stack[str]()
stack.push('Item 1')
stack.push('Item 2')
print(stack.pop())
print(stack.pop())
print(stack.pop())
Это определение выглядит весьма многословно и, кроме того, не позволяет уточнять, что значение типа должно быть отнаследовано от какого-то базового типа. В действительности базовый тип можно определить через аргумент bound в typing.TypeVar
(с уточнением covariant=True), но в целом синтаксис получается не самым простым и очевидным.
PEP695 определяет упрощенный синтаксис для generic-типов, который позволяет указывать на использование обобщенного типа в функции или классе с помощью квадратных скобок после названия функции или класса. Наше определение стека теперь будет выглядеть таким образом:
class Stack[T]:
def __init__(self):
self._data:list[T] = []
def push(self, item:T):
self._data.append(item)
def pop(self) -> T | None:
if self._data:
return self._data.pop()
else:
return None
stack = Stack[str]()
stack.push('Item 1')
stack.push('Item 2')
print(stack.pop())
print(stack.pop())
print(stack.pop())
Также можно указывать возможные подтипы для обобщенного типа через T: base
. Также можно указывать перечисление возможных типов (например, int | float
), как в определении типа через type, так и в указании базового типа. Также обобщенные типы могут использоваться при наследовании типов (например, стек можно создать как подтипы class Stack[T](list[T])
. Допускается использовать также протоколы (typing.Protocol
как базовый класс) для определения допустимых типов объекта не только через прямое наследование, но и также через реализацию необходимого интерфейса. Например, может быть создан класс с методом explain() и указан как базовый тип для списка:
class Explainable(typing.Protocol):
def explain(self) -> str:
pass
class Stack[T:Explainable]:
# определение класса стека
class Animal:
def explain(self) -> str:
return "I'm an animal"
animals = Stack[Animal]()
animals.push(Animal())
Расширение также добавляет новый атрибут в типы абстрактного синтаксического дерева ClassDef, FunctionDef, AsyncFunctionDef для уточнения связанного типа и его ограничений.
Статья подготовлена в преддверии старта курса Python Developer.Professional.