Первоначально 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)
tumbler
19.05.2023 14:28+5Это настолько похоже на дженерики в go, что я даже не понял из статьи, что именно там новое. Действительно, питон кродётся в сторону суслика...
BeardedBeaver
19.05.2023 14:28+13Вы уверены, что приведенный в статье синтаксис верный?
class Stack[T]: _data: List<T> = []
Я бегло посмотрел пропозал по ссылке и там написано, что угловые скобки решили не использовать потому что они чужеродны устоявшейся системе указания типов
ammo
19.05.2023 14:28+5Тоже покоробило это. Автор в одной строчке пишет нормально через [ ] и в следующей его резко заносит в куда-то в жаву с треугольными скобками. В питоне такого нет и было бы глупо вводить
danilovmy
19.05.2023 14:28+2ахаха, хором про java.
dmitriizolotov Автор
19.05.2023 14:28-1Опечатка у меня конечно же, прошу прощения. При финальном редактировании думал о рабочих задачах (на Dart/Kotlin), в голове синтаксис перемешался. Исправил текст, спасибо что заметили!
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 и линтеры укажут разработчику сразу на три пропущенных пробела.В общем, типичная реклама курсов.
dmitriizolotov Автор
19.05.2023 14:28Нет, копипасты не было, там первая часть скорее про рассмотрение как это было в старых версиях Python. Ниже есть упоминание про альтернативные типы (ну и в документации не очень рекомендуют использовать тип | None, хотя и считают этот синтаксис допустимым).
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;
dmitriizolotov Автор
19.05.2023 14:28+1тут была у меня опечатка, скобки конечно же квадратные List[T], при финальном редактировании ошибся и в голове смешался синтаксис :( Но так да, более того при разработке PEP (там в конце документа можно увидеть), что сравнивали реализации в других языках (и даже предлагали использовать угловые скобки).
danilovmy
19.05.2023 14:28+2Стоп. Я чего-то не понял. Атрибут лист у класса? Тогда каждый инстанс правит этот атрибут. Ээээ что? Автор, поясни пожалуйста. Может я что-то попутал или это специально так сделано в примере?
santjagocorkez
19.05.2023 14:28+1Да, классовый атрибут (один экземпляр на все экземпляры самого класса), да еще и мутабельный. Вас в ОТУС еще и не такому научат. Тут вот, например, учили, как противоречить самому себе и не подавать виду, что психиатрическая лечебница не то же самое, что офис на верхнем этаже эмпайр стейт билдинга.
dmitriizolotov Автор
19.05.2023 14:28Исправил, сейчас в примерах сделано корректно с хранением данных в инстансе.
gnomeby
19.05.2023 14:28Подобные аннотации помогают IDE определять список допустимых операций и
проверять корректность использования переменных и типа возвращаемого
значения.Не скажу за все IDE, но VSCode уже не нуждается в аннотациях чтобы делать всё вышеперечисленное.
dx-77
19.05.2023 14:28В Python 3.9 и выше вместо typing.List[str] и typing.Dict[str, str] можно писать list[str] и dict[str,str]
baldr
Ох, куда-то не туда пошел Питон..
Если раньше с первого взгляда было видно что это, например, вызов функции из массива, то сейчас меняется сам синтаксис и уже надо выяснять - а не дженерик-ли здесь..
redfox0
Вот-вот. Чем такой синтаксис не угодил?
Или так (return type polymorphism):