(我对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
的参数类型)。所以界面太通用了——命令不需要与任何运行器一起工作,只需要与某些运行器一起工作。因此,让我们在第二个类型 var 上使 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]
我如何对其进行类型注释,以便可以进行上述扩展,但仍然可以正确进行类型检查?
现在的注释确实是矛盾的:
Runner
仅允许Command
形式的Command[T, Runner[T]]
。execute
的Command[bool, Runner[bool]]
方法接受anyRunner[bool]
。execute
的MyCommand
方法仅接受任何带有Runner[bool]
的“magic_level
”。因此,
MyCommand
不是Command[bool, Runner[bool]]
——它不接受任何“Runner[bool]
没有magic_level
”。这迫使 MyPy 拒绝替换,即使其原因发生得更早。
这个问题可以通过将
R
参数化为Runner
的自类型来解决。这避免了强制 Runner
通过 baseclass
Command
来参数化 Runner[T]
,而是通过 Runner[T]
的实际 subtype来参数化它。
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)
您需要将实例变量添加到
Runner
接口:
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
T = TypeVar('T')
class Command(ABC, Generic[T]):
@abstractmethod
def execute(self, runner: Runner[T]) -> T:
pass
class Runner(ABC, Generic[T]):
magic_level: int # <- note the change to your code here!
def run(self, command: Command[T]) -> T:
return command.execute(self)
那么您的第一个实现将正常运行。我觉得您心里也有一个包含实例变量的接口。当然,如果
magic_level
也是 TypeVar
或者它具有更开放的 Protocol
的特点,那么整个事情也有效。