Python typing generic types

AlexKorablev.ru

AlexKorablev.ru

Александр Кораблев о разработке ПО, ИТ-индустрии и Python.

Использование typing.Generic в Python

Опубликовано 18 February 2022 в Python

Я работаю над проектом с довольно большой кодовой базой. Проект с историей. Некоторые части наша команда написала задолго до аннотаций типов. Мы до сих пор добавляем их в наш легаси код и улучшаем существующие подсказки. Стоит эта игра свеч? Определенно. Наши пользователи — разработчики. Они открывают наш код в PyCharm ежедневно. И они надеются, что он поможет им решить их задачи максимально быстро и просто.

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

Один из моих коллег добавил аннотацию типов, которая выглядит так:

# file name: type_cast_0.py class A: a = 'a' class B(A): b = 'b' class DoSomethingWithA: _class = A def do(self) -> A: return self._class() class DoSomethingWithB(DoSomethingWithA): _class = B 

PyCharm не видит проблем в этом коде. Его анализатор показывает зеленую галочку. Mypy также не находит никаких проблем:

$ mypy type_cast_0.py Success: no issues found in 1 source file 

Но если добавить вот такой код, который использует DoSomethingWithB .

# file name: type_cast_1.py from type_cast_0 import DoSomethingWithB print(DoSomethingWithB().do().b) 

PyCharm теперь показывает warning: Unresolved attribute reference ‘b’ for class ‘A’ . И Mypy помечает этот кусок кода ошибкой.

$ mypy type_cast_1.py type_cast_1.py:4: error: "A" has no attribute "b" Found 1 error in 1 file (checked 1 source file) 

Попробуем это исправить. Ниже моя первая попытка: наивный подход к дженерикам в Питоне.

# file name: type_cast_2.py #. TV = tp.TypeVar('TV') class DoSomethingWithA(tp.Generic[TV]): _class: tp.Type[TV] = A def do(self) -> TV: return self._class() class DoSomethingWithB(DoSomethingWithA): _class = B 

PyCharm не показывает никаких ошибок или предупреждений. Mypy все еще не нравится мой код.

$ mypy type_cast_3.py type_cast_2.py:17: error: Incompatible types in assignment (expression has type "Type[A]", variable has type "Type[TV]") Found 1 error in 1 file (checked 1 source file) 

Интересно… Попробуем поменять TV = tp.TypeVar(‘TV’) на TV = tp.TypeVar(‘TV’, bound=A) . Такая же ошибка. Становится интереснее…

Официальная документация не сильно помогает. В ней всего пара примеров использования Generics, но ничего, что даст ключ к исправлению проблемы. К счастью, есть прекрасный раздел о Generics в документации mypy.

Для моего примера, код может выглядеть как-то так.

# file name: type_cast_6.py # . class DoSomethingWith(tp.Generic[TV]): _class: tp.Type[TV] def do(self) -> TV: return self._class() 

А вот пример его использования.

# file name: type_cast_7.py from type_cast_6 import DoSomethingWith, B print(DoSomethingWith[B]().do().b) 

Mypy не видит никаких проблем. PyCharm показывает зеленую галочку.

$ mypy type_cast_6.py Success: no issues found in 1 source file $ mypy type_cast_7.py Success: no issues found in 1 source file 

К сожалению, попытка выполнить этот код завалится с исключением.

$ python type_cast_7.py . AttributeError: 'DoSomethingWith' object has no attribute '_class' 

В питоне нет возможности использовать TypeVar так же, как можно использовать дженерики в Java, на пример. Я не могу присвоить TV переменной _class и ожидать, что питон во заменит переменную типа на реальный класс во время выполнения. Другими словами, если использовать _class: tp.Type[TV] = TV в type_cast_6.py , я получу TypeError: ‘TypeVar’ object is not callable .

Что бы этого избежать я добавил подклассы для DoSomethingWith .

# file name: type_cast_8.py # . class DoSomethingWithA(DoSomethingWith): _class = A class DoSomethingWithB(DoSomethingWith): _class = B 
# file name: type_cast_9.py from type_cast_8 import DoSomethingWithB print(DoSomethingWithB().do().b) 

Не особенно элегантное решение, но оно работает.

В этом посте много примеров кода. Я их порезал. Полные примеры можно найти в специальном репозитории на моем аккаутне в GitHub.

Понравилась статья? Поделись с друзьями!

Источник

Новый синтаксис для 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: 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.

Источник

Читайте также:  Вывод диалогового окна java
Оцените статью