具有自类型的通用协议中的类方法,mypy 类型检查失败 [英] Classmethods in Generic Protocols with self-types, mypy type checking failure

查看:64
本文介绍了具有自类型的通用协议中的类方法,mypy 类型检查失败的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

A little background, I essentially need to define an int wrapper type, say MyInt (among some other classes), and another generic Interval type which can accept MyInt objects as well as other types of objects. Since the types acceptable by the Interval do not fall into a neat hierarchy, I thought this would be a perfect use-case for the experimental Protocol, which in my case would require a couple of methods and a couple of @classmethods. All the methods return a "self-type", i.e., MyInt.my_method returns a MyInt. Here is a 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))

然而,mypy 抱怨:

However, mypy complains:

(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

这对我来说很难理解.为什么返回类型需要 ?我尝试在协议中不注释 cls :

Which to me is hard to understand. Why is return-type expecting <nothing>? I tried simply not annotating cls in the protocol:

_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:
        ...

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

However, mypy complains with a similar error message:

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

对我来说,这更没有意义.请注意,如果我使用这些实例方法:

Which to me, makes even less sense. Note, if I make these instance methods:

_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)

然后,mypy 根本没有抱怨:

Then, mypy doesn't complain at all:

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

I've read about self-types in protocols in PEP 544, where it gives the following example:

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

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

Furthermore, in PEP484, regarding typing classmethods, we see this example:

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

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

What is wrong with my Protocol / class definition? Am I missing something obvious? I would appreciate any specific answers about why this is failing, or any work-around. But note, I need these attributes to be accessible on the class.

注意,我已经尝试使用 ClassVar,但这引入了其他问题......即,ClassVar 不接受类型变量 据我所知 ClassVar 不能是通用的.理想情况下,它会是一个 @classmethod,因为我可能不得不依赖其他我想在类中推送的元数据.

Note, I've tried using a ClassVar, but that introduced other issues... namely, ClassVar does not accept type-variables as far as I can tell ClassVar's cannot be generic. And ideally, it would be a @classmethod since I might have to rely on other meta-data I would want to shove in the class.

推荐答案

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

I'm not on expert on Mypy but have been teaching myself to use it recently and I think this may be due to an issue in Mypy mentioned here:

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

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

The issue is with handling TypeVar variables in class methods rather than anything directly to do with protocols.

链接中给出了以下最小示例以显示问题.

The following minimal example is given in the link to show the problem.

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

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

The output from reveal_type is T rather than str. The same thing is happening with your code, where Mypy is failing to infer the type should be MyInteger rather than _P and so doesn't see your class as implementing the protocol. Changing the return type of the class methods to 'PType' makes the errors go away, but I'm not confident enough to know if there are any other impacts of that change.

有一些关于如何最好地处理它的讨论,因为在每种情况下决定正确的行为应该是很重要的,所以向他们标记这个以获得更多用例示例可能没有害处(请参阅https://github.com/python/mypy/issues/5664 例如.)

There's been some discussion on how best to handle it, because it's not trivial to decide what the correct behaviour should be in every case, so might be no harm flagging this to them for more use case examples (see https://github.com/python/mypy/issues/5664 for example.)

这篇关于具有自类型的通用协议中的类方法,mypy 类型检查失败的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆