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

Комментарии (18)


  1. baldr
    19.05.2023 14:28
    +17

    Ох, куда-то не туда пошел Питон..

    animals = Stack[Animal]()

    Если раньше с первого взгляда было видно что это, например, вызов функции из массива, то сейчас меняется сам синтаксис и уже надо выяснять - а не дженерик-ли здесь..


    1. redfox0
      19.05.2023 14:28

      Вот-вот. Чем такой синтаксис не угодил?

      animals = Stack.<Animal>()
      

      Или так (return type polymorphism):

      animals: Stack<Animal> = Stack()
      


  1. tumbler
    19.05.2023 14:28
    +5

    Это настолько похоже на дженерики в go, что я даже не понял из статьи, что именно там новое. Действительно, питон кродётся в сторону суслика...


  1. BeardedBeaver
    19.05.2023 14:28
    +13

    Вы уверены, что приведенный в статье синтаксис верный?

    class Stack[T]:
      _data: List<T> = []

    Я бегло посмотрел пропозал по ссылке и там написано, что угловые скобки решили не использовать потому что они чужеродны устоявшейся системе указания типов


    1. ammo
      19.05.2023 14:28
      +5

      Тоже покоробило это. Автор в одной строчке пишет нормально через [ ] и в следующей его резко заносит в куда-то в жаву с треугольными скобками. В питоне такого нет и было бы глупо вводить


      1. danilovmy
        19.05.2023 14:28
        +2

        ахаха, хором про java.


        1. dmitriizolotov Автор
          19.05.2023 14:28
          -1

          Опечатка у меня конечно же, прошу прощения. При финальном редактировании думал о рабочих задачах (на Dart/Kotlin), в голове синтаксис перемешался. Исправил текст, спасибо что заметили!


    1. ri_gilfanov
      19.05.2023 14:28
      +8

      Добавим сюда упоминание typing.Optional в 2023 году, когда под Python 3.10 и выше вместо Optional[int] можно писать int | None. При том, что этот синтаксис упоминается ниже как допустимый аргумент дженериков в Python 3.12.

      Похоже начало статьи -- это копипаста из какого-то старого материала.

      Ещё смущает строчка c:int = a+b -- это корректный синтаксис Python, но многие IDE и линтеры укажут разработчику сразу на три пропущенных пробела.

      В общем, типичная реклама курсов.


      1. dmitriizolotov Автор
        19.05.2023 14:28

        Нет, копипасты не было, там первая часть скорее про рассмотрение как это было в старых версиях Python. Ниже есть упоминание про альтернативные типы (ну и в документации не очень рекомендуют использовать тип | None, хотя и считают этот синтаксис допустимым).


  1. danilovmy
    19.05.2023 14:28
    +1

    Постойте, эти люди решили изобрести Java?
    Было:


    # python
    class Stack:
      _data = []  // Мы изначально знаем что это лист.

    Стало:


    # python
    class Stack[T]:
      _data: List<T> = []  // Кто то считает, что мы изначально не знаем что это лист.

    ну и Java:


    // java
    class Stack {
            List<T> _data;
    


    1. HemulGM
      19.05.2023 14:28
      +1

      Покину Делфи:

      TStack<T> = class
        data: TList<T>;


    1. dmitriizolotov Автор
      19.05.2023 14:28
      +1

      тут была у меня опечатка, скобки конечно же квадратные List[T], при финальном редактировании ошибся и в голове смешался синтаксис :( Но так да, более того при разработке PEP (там в конце документа можно увидеть), что сравнивали реализации в других языках (и даже предлагали использовать угловые скобки).


  1. danilovmy
    19.05.2023 14:28
    +2

    Стоп. Я чего-то не понял. Атрибут лист у класса? Тогда каждый инстанс правит этот атрибут. Ээээ что? Автор, поясни пожалуйста. Может я что-то попутал или это специально так сделано в примере?


    1. santjagocorkez
      19.05.2023 14:28
      +1

      Да, классовый атрибут (один экземпляр на все экземпляры самого класса), да еще и мутабельный. Вас в ОТУС еще и не такому научат. Тут вот, например, учили, как противоречить самому себе и не подавать виду, что психиатрическая лечебница не то же самое, что офис на верхнем этаже эмпайр стейт билдинга.


    1. dmitriizolotov Автор
      19.05.2023 14:28

      Исправил, сейчас в примерах сделано корректно с хранением данных в инстансе.


  1. gnomeby
    19.05.2023 14:28

    Подобные аннотации помогают IDE определять список допустимых операций и
    проверять корректность использования переменных и типа возвращаемого
    значения.

    Не скажу за все IDE, но VSCode уже не нуждается в аннотациях чтобы делать всё вышеперечисленное.


    1. me21
      19.05.2023 14:28

      PyCharm туда же.


  1. dx-77
    19.05.2023 14:28

    В Python 3.9 и выше вместо typing.List[str] и typing.Dict[str, str] можно писать list[str] и dict[str,str]