Использование 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.
Возник вопрос? Мне всегда можно написать в Twitter: avkorablev