Новый синтаксис для generic-типов в Python 3.12

от автора

Первоначально 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *