具有自我类型的通用协议中的类方法,mypy类型检查失败

jua*_*aga 8 python class-method structural-typing python-3.x mypy

一点背景,我基本上需要定义一个int包装类型,比如说MyInt(在其他一些类中),以及另一个Interval可以接受MyInt对象以及其他类型对象的泛型类型.由于不可接受的类型Interval不属于整洁的层次结构,我认为这将是实验的完美用例Protocol,在我的情况下需要几个方法和几个@classmethods.所有方法都返回一个"自我类型",即MyInt.my_method返回一个MyInt.这是一个MCVE:

from dataclasses import dataclass
from typing import Union, ClassVar, TypeVar, Generic, Type

from typing_extensions import Protocol


_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @classmethod
    def maximum_type_value(cls: Type[_P]) -> _P:
        ...
    @classmethod
    def minimum_type_value(cls: Type[_P]) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    @classmethod
    def maximum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MAX)
    @classmethod
    def minimum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)


@dataclass
class Interval(Generic[_P]):
    low: _P
    high: _P

interval = Interval(MyInteger(1), MyInteger(2))
def foo(x: PType) -> PType:
    return x
foo(MyInteger(42))
Run Code Online (Sandbox Code Playgroud)

但是,mypy抱怨说:

(py37) Juans-MacBook-Pro: juan$ mypy mcve.py
mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def maximum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def minimum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger
Run Code Online (Sandbox Code Playgroud)

对我来说很难理解.为什么回归型期待<nothing>?我试过cls在协议中没有注释:

_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @classmethod
    def maximum_type_value(cls) -> _P:
        ...
    @classmethod
    def minimum_type_value(cls) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...
Run Code Online (Sandbox Code Playgroud)

但是,mypy抱怨类似的错误消息:

mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] maximum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] minimum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger
Run Code Online (Sandbox Code Playgroud)

对我来说,哪个更没意义.注意,如果我创建这些实例方法:

_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    def maximum_type_value(self: _P) -> _P:
        ...
    def minimum_type_value(self: _P) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    def maximum_type_value(self) -> MyInteger:
        return MyInteger(self._MAX)
    def minimum_type_value(self) -> MyInteger:
        return MyInteger(self._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)
Run Code Online (Sandbox Code Playgroud)

然后,mypy根本不抱怨:

在PEP 544中已经阅读了协议中的自我类型,其中给出了以下示例:

C = TypeVar('C', bound='Copyable')
class Copyable(Protocol):
    def copy(self: C) -> C:

class One:
    def copy(self) -> 'One':
        ...

T = TypeVar('T', bound='Other')
class Other:
    def copy(self: T) -> T:
        ...

c: Copyable
c = One()  # OK
c = Other()  # Also OK
Run Code Online (Sandbox Code Playgroud)

此外,在PEP484中,关于键入classmethods,我们看到这个例子:

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D
Run Code Online (Sandbox Code Playgroud)

我的Protocol/ class定义有什么问题?我错过了一些明显的东西吗 我将不胜感激任何具体的答案,为什么这是失败,或任何解决方法.但请注意,我需要在课堂上访问这些属性.

注意,我已经尝试过使用a ClassVar,但是引入了其他问题...即,我ClassVar不能接受类型变量,因为我不能说ClassVar是通用的.理想情况下,这将是一个@classmethod因为我可能不得不依赖于我想要在课堂上推动的其他元数据.

And*_*ell 4

我不是 Mypy 方面的专家,但最近一直在自学使用它,我认为这可能是由于这里提到的 Mypy 中的一个问题造成的:

https://github.com/python/mypy/issues/3645

问题在于处理类方法中的 TypeVar 变量,而不是与协议直接相关的任何内容。

链接中给出了以下最小示例来显示该问题。

T = TypeVar('T')

class Factory(Generic[T]):
    def produce(self) -> T:
        ...
    @classmethod
    def get(cls) -> T:
        return cls().produce()

class HelloWorldFactory(Factory[str]):
    def produce(self) -> str:
        return 'Hello World'

reveal_type(HelloWorldFactory.get())  # mypy should be able to infer 'str' here
Run Code Online (Sandbox Code Playgroud)

Reveal_type 的输出是 T 而不是 str。您的代码也发生了同样的事情,Mypy 无法推断类型应该是MyInteger而不是_P,因此不会将您的类视为实现协议。更改类方法的返回类型以'PType'使错误消失,但我没有足够的信心知道该更改是否有任何其他影响。

关于如何最好地处理它进行了一些讨论,因为决定每种情况下正确的行为应该是什么并不简单,因此向他们标记这一点以获取更多用例示例可能没有什么坏处(请参阅https://github.com例如/python/mypy/issues/5664 。)