
Первоначально 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: _data: List<str> = [] def push(self, item:str): self._data.append(item) def pop(self) -> Optional[str]: if self._data: item = self._data[-1] self._data = self._data[:-1] return item else: return None
Это будет успешно работать со строками, но как определить стек для произвольных значений? PEP646 определил возможность создавать обобщенные типы (typing.TypeVar) и определение стека через них может быть выполнено следующим образом:
StackType = TypeVar('StackType') class Stack(Generic[StackType]): _data: List<StackType> = [] def push(self, item:StackType): self._data.append(item) def pop(self) -> Optional[StackType]: if self._data: item = self._data[-1] self._data = self._data[:-1] return item else: return None
Это определение выглядит весьма многословно и, кроме того, не позволяет уточнять, что значение типа должно быть отнаследовано от какого-то базового типа. В действительности базовый тип можно определить через аргумент bound в typing.TypeVar (с уточнением covariant=True), но в целом синтаксис получается не самым простым и очевидным.
PEP695 определяет упрощенный синтаксис для generic-типов, который позволяет указывать на использовать обобщенного типа в функции или классе с помощью квадратных скобок после названия функции или класса. Наше определение стека теперь будет выглядеть таким образом:
class Stack[T]: _data: List<T> = [] def push(self, item:T): self._data.append(item) def pop(self) -> Optional[T]: if self._data: item = _self.data[-1] self._data = self._data[:-1] return item else: return None
Также можно указывать возможные подтипы для обобщенного типа через 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.
ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/736244/
Добавить комментарий