Я работаю над проектом с довольно большой кодовой базой. Проект с историей.
Некоторые части наша команда написала задолго до аннотаций типов. Мы до сих пор добавляем их в наш легаси‑код и улучшаем существующие подсказки. Стоит ли эта игра свеч? Определённо — да.
Наши пользователи — разработчики. Они открывают наш код в 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)
Попробуем это исправить. Ниже — моя первая попытка: наивный подход к дженерикам в Python.
# 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'
В Python нет возможности использовать TypeVar так же, как можно использовать дженерики в Java, например. Я не могу присвоить TV переменной _class и ожидать, что Python заменит переменную типа на реальный класс во время выполнения. Другими словами, если использовать _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.