由共享的任意类型耦合的抽象 类 的类型注释

Type annotations for abstract classes that are coupled by a shared, arbitrary type

(我对 Python 的类型注释和 mypy 比较陌生,所以我详细描述了我的问题以避免 运行 变成 XY问题)

我有两个抽象 classes,它们交换任意但固定类型的值:

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 subclass需要访问我的Runner subclass的一个属性(假设命令可以适应具有不同能力的跑步者):

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 的参数类型) .所以界面太通用了——一个命令不需要与任何跑步者一起工作,只需要与一些跑步者一起工作。因此,让我们在第二个类型 var 上使 Command 通用,以捕获受支持的跑步者 class:

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 is a subclass of Command[bool, MyRunner], MyRunner is a subclass Runner[bool],那么为什么 MyCommandCommand[bool, Runner[bool]] 不兼容?

如果 mypy 满意我可能会实现一个 Command subclass 和一个 Runner subclass 为 [=33 使用“不同的值” =](因为 R 没有绑定到 T)而没有 mypy 抱怨。我尝试了 R = TypeVar('R', bound='Runner[T]'),但又引发了另一个错误:

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

我如何对其进行类型注释,以便如上所述的扩展是可能的,但仍然可以正确进行类型检查?

目前的注解确实自相矛盾:

  • Runner 仅允许 Command 形式为 Command[T, Runner[T]]
  • Command[bool, Runner[bool]]execute 方法接受 any Runner[bool].
  • MyCommandexecute方法只接受任何“Runner[bool]带有magic_level”。

因此,MyCommand 不是 Command[bool, Runner[bool]] – 它不接受任何“Runner[bool] 没有 magic_level” .这会强制 MyPy 拒绝替换,即使它的原因发生得更早。


这个问题可以通过将 R 参数化为 Runner 的自身类型来解决。这避免了强制 Runner 基类 Runner[T] 参数化 Command,而是通过实际的 子类型 [=48] 对其进行参数化=] 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)