Как стать автором
Обновить
1117.4
OTUS
Цифровые навыки от ведущих экспертов

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

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров18K

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 19: ↑11 и ↓8+7
Комментарии18

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS