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

问题描述 投票:0回答:2

(我对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]

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

python python-typing mypy
2个回答
1
投票

现在的注释确实是矛盾的:

  • Runner
    仅允许
    Command
    形式的
    Command[T, Runner[T]]
  • execute
    Command[bool, Runner[bool]]
    方法接受any
    Runner[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)

1
投票

您需要将实例变量添加到

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
的特点,那么整个事情也有效。

© www.soinside.com 2019 - 2024. All rights reserved.