Аннотации типов для абстрактных классов, которые связаны общим произвольным типом

(Я новичок в аннотациях типов Python и mypy, поэтому подробно описываю свою проблему, чтобы не столкнуться с проблемой XY)

У меня есть два абстрактных класса, которые обмениваются значениями произвольного, но фиксированного типа:

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Generic, TypeVar


T = TypeVar('T')  # result type


class Command(ABC, Generic[T]):
    @abstractmethod
    def execute(self, runner: Runner[T]) -> T:
        raise NotImplementedError()


class Runner(ABC, Generic[T]):
    def run(self, command: Command[T]) -> T:
        return command.execute(self)

В моей реализации этого интерфейса подкласс Command должен получить доступ к атрибуту моего подкласса Runner (представьте, что команда может адаптироваться к бегунам с разными возможностями):

class MyCommand(Command[bool]):
    def execute(self, runner: Runner[bool]) -> bool:
        # Pseudo code to illustrate dependency on runner's attributes
        return runner.magic_level > 10


class MyRunner(Runner[bool]):
    magic_level: int = 20

Это работает, как ожидалось, но не удовлетворяет mypy:

mypy_sandbox.py:24: error: "Runner[bool]" has no attribute "magic_level"  [attr-defined]

Очевидно, mypy верен: атрибут magic_level определен в MyRunner, но не в Runner (который является типом аргумента для execute). Так что интерфейс слишком общий - команда не должна работать с каким-либо бегуном, только с некоторыми бегунами. Итак, давайте сделаем Command универсальным для переменной второго типа, чтобы захватить поддерживаемый класс бегуна:

R = TypeVar('R')  # runner type
T = TypeVar('T')  # result type


class Command(ABC, Generic[T, R]):
    @abstractmethod
    def execute(self, runner: R) -> T:
        raise NotImplementedError()


class Runner(ABC, Generic[T]):
    def run(self, command: Command[T, Runner[T]]) -> T:
        return command.execute(self)


class MyCommand(Command[bool, MyRunner]):
    def execute(self, runner: MyRunner) -> bool:
        # Pseudo code to illustrate dependency on runner's attributes
        return runner.magic_level > 10


# MyRunner defined as before

Это удовлетворяет mypy, но когда я пытаюсь использовать код, mypy снова жалуется:

if __name__ == '__main__':
    command = MyCommand()
    runner = MyRunner()
    print(runner.run(command))
mypy_sandbox.py:35: error: Argument 1 to "run" of "Runner" has incompatible type "MyCommand"; expected "Command[bool, Runner[bool]]"  [arg-type]

На этот раз я даже не понимаю ошибку: MyCommand является подклассом Command[bool, MyRunner], а MyRunner является подклассом Runner[bool], так почему MyCommand несовместим с Command[bool, Runner[bool]]?

И если mypy был удовлетворен, я, вероятно, мог бы реализовать подкласс Command с подклассом Runner, который использует другое значение для T (поскольку R не привязан к T) без жалоб mypy. Я пробовал R = TypeVar('R', bound='Runner[T]'), но это вызывает еще одну ошибку:

error: Type variable "mypy_sandbox.T" is unbound  [valid-type]

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


person Florian Brucker    schedule 18.11.2020    source источник
comment
В чем-то вроде Java вы бы использовали class Command<T, R extends Runner<T>>, но я не думаю, что аннотации типов Python поддерживают переменные типа с общими границами.   -  person user2357112 supports Monica    schedule 18.11.2020


Ответы (1)


Текущие аннотации действительно противоречат друг другу:

  • Runner допускает только Command формы Command[T, Runner[T]].
  • execute метод Command[bool, Runner[bool]] принимает любые Runner[bool].
  • execute метод MyCommand принимает только любые Runner[bool] с magic_level.

Следовательно, MyCommand не является Command[bool, Runner[bool]] - он не принимает никаких Runner[bool] без magic_level. Это заставляет MyPy отклонить замену, даже если причина для этого произошла раньше.


Эта проблема может быть решена путем параметризации R в качестве собственного типа Runner. Это позволяет избежать принуждения Runner к параметризации Command базовым классом Runner[T], а вместо этого параметризует его фактическим подтипом Runner[T].

R = TypeVar('R', bound='Runner[Any]')
T = TypeVar('T')  # result type

class Command(ABC, Generic[T, R]):
    @abstractmethod
    def execute(self, runner: R) -> T:
        raise NotImplementedError()


# Runner is not generic in R
class Runner(ABC, Generic[T]):
    # Runner.run is generic in its owner
    def run(self: R, command: Command[T, R]) -> T:
        return command.execute(self)
person MisterMiyagi    schedule 18.11.2020
comment
Но тогда вы можете написать такие вещи, как Command[int, str], где второй параметр типа вообще не является бегуном. - person user2357112 supports Monica; 18.11.2020
comment
@ user2357112supportsMonica Действительно, я пропустил, что мое определение R отличается от того, которое использовалось в вопросе. См. Последнее изменение. - person MisterMiyagi; 18.11.2020
comment
Спасибо за ваш вклад! Однако ваш пример не проверяет тип для меня в его текущем состоянии: mypy (0.790) говорит `Отсутствуют параметры типа для универсального типа Runner` в определении R. - person Florian Brucker; 19.11.2020
comment
@FlorianBrucker Я также использую MyPy 0.790. Можете ли вы сравнить с полным кодом? - person MisterMiyagi; 19.11.2020
comment
Ошибка исчезнет, ​​если я установлю для disallow_any_generics значение False ( дефолт). Но даже если мы проигнорируем эту проблему, ваш код по-прежнему позволяет мне создавать use class MyBrokenCommand(Command[float, MyRunner]) без жалоб проверщика типов, потому что R не привязан к T. - person Florian Brucker; 20.11.2020
comment
@FlorianBrucker Да, и первоначальная спецификация тоже. Если команда знает, как получить T, не используя Runner, это нормально. MyRunner, однако, откажется run, поскольку для этого требуется Command[bool, ...]. - person MisterMiyagi; 20.11.2020
comment
Это правда, и, вероятно, достаточно хорошо. Я все еще хотел бы, чтобы это работало с включенным disallow_any_generics. У вас есть идеи на этот счет? - person Florian Brucker; 20.11.2020
comment
@FlorianBrucker Очевидно, MyPy ожидает R = TypeVar('R', bound='Runner[Any]'). - person MisterMiyagi; 20.11.2020