【问题标题】:Generic protocols: mypy error: Argument 1 has incompatible type ...; expected通用协议:mypy 错误:参数 1 具有不兼容的类型 ...;预期的
【发布时间】:2021-09-19 22:43:45
【问题描述】:

我正在尝试实现一个通用协议。我的意图是拥有一个带有简单 getter 的 Widget[key_type, value_type] 协议。 Mypy 抱怨Protocol[K, T],因此变成了Protocol[K_co, T_co]。我已经去除了所有其他限制,但我什至无法让最基本的情况widg0: Widget[Any, Any] = ActualWidget() 工作。 ActualWidget.get 应该与get(self, key: K) -> Any 完全兼容,这让我觉得我在某种程度上使用了泛型/协议错误,或者 mypy 无法处理。

来自 mypy 的命令/错误:

$ mypy cat_example.py
cat_example.py:34: error: Argument 1 to "takes_widget" has incompatible type "ActualWidget"; expected "Widget[Any, Any]"
cat_example.py:34: note: Following member(s) of "ActualWidget" have conflicts:
cat_example.py:34: note:     Expected:
cat_example.py:34: note:         def [K] get(self, key: K) -> Any
cat_example.py:34: note:     Got:
cat_example.py:34: note:         def get(self, key: str) -> Cat
Found 1 error in 1 file (checked 1 source file)

或者,如果我尝试使用 widg0: Widget[Any, Any] = ActualWidget() 强制分配:

error: Incompatible types in assignment (expression has type "ActualWidget", variable has type "Widget[Any, Any]")

完整代码:

from typing import Any, TypeVar
from typing_extensions import Protocol, runtime_checkable

K = TypeVar("K")  # ID/Key Type
T = TypeVar("T")  # General type
K_co = TypeVar("K_co", covariant=True)  # ID/Key Type or subclass
T_co = TypeVar("T_co", covariant=True)  # General type or subclass
K_contra = TypeVar("K_contra", contravariant=True)  # ID/Key Type or supertype
T_contra = TypeVar("T_contra", contravariant=True)  # General type or supertype

class Animal(object): ...

class Cat(Animal): ...


@runtime_checkable
class Widget(Protocol[K_co, T_co]):
    def get(self, key: K) -> T_co: ...

class ActualWidget(object):
    def get(self, key: str) -> Cat:
        return Cat()

def takes_widget(widg: Widget):
    return widg

if __name__ == '__main__':
    widg0 = ActualWidget()
    #widg0: Widget[str, Cat] = ActualWidget()
    #widg0: Widget[Any, Any] = ActualWidget()

    print(isinstance(widg0, Widget))
    print(isinstance({}, Widget))
    takes_widget(widg0)

【问题讨论】:

  • 我认为您需要做的就是将 Widget 协议中的 typehint 更改为 str。没有?
  • 不,小部件是通用的。
  • 我不认为更改 key 的类型会使 Widget 成为非通用的。 key可以是什么类型?
  • 你能确认这就是你想要的吗? mypy-play.net/…。所有类型检查,take_widget 有效地采用Widget[Any, Any](您的示例的主要错误是您使用K 作为键,但K_co 作为Protocol 的参数)。如果游乐场 sn-p 是您想要的行为,我会写一个答案。
  • @MarioIshac 这看起来很有希望!当然比我得到的更远。我注意到对于协议,mypy “希望” 参数类型“更矛盾”,返回类型“更协变”。我曾尝试对两者都使用K_co,但这似乎会导致更多错误。这至少给了我一个协议和一个函数,我可以用相同的类型来加强它!

标签: python generics types mypy structural-typing


【解决方案1】:

把我在 cmets 里的东西放在这里。

要使您的问题的示例有效,您需要像这样使输入参数逆变和输出参数协变

from typing import TypeVar
from typing_extensions import Protocol, runtime_checkable

T_co = TypeVar("T_co", covariant=True)  # General type or subclass
K_contra = TypeVar("K_contra", contravariant=True)  # ID/Key Type or supertype

class Animal: ...

class Cat(Animal): ...

@runtime_checkable
class Widget(Protocol[K_contra, T_co]):
    def get(self, key: K_contra) -> T_co: ...

class ActualWidget:
    def get(self, key: str) -> Cat:
        return Cat()

def takes_widget(widg: Widget):
    return widg

class StrSub(str):
    pass

if __name__ == '__main__':
    widget_0: Widget[str, Cat] = ActualWidget()
    widget_1: Widget[StrSub, Cat] = ActualWidget()
    widget_2: Widget[str, object] = ActualWidget()
    widget_3: Widget[StrSub, object] = ActualWidget()

    takes_widget(widget_0)
    takes_widget(widget_1)
    takes_widget(widget_2)
    takes_widget(widget_3)

ActualWidget(),它是一个Widget[str, Cat],然后可分配给Widget[SubStr, object] 用于widget_3,这意味着Widget[str, Cat]Widget[SubStr, object] 的子类。

Widget[str, Cat] 可以采用所有 SubStrs 以及其他 str 子类型(sublcas 关系中的输入类型可能不太具体,因此是逆变的)并且可以具有至少为 object 的输出,并且具有str 属性(子类关系中的输出类型可以更具体,因此是协方差)。另见Wikipedia - Function Types,它正式化了这一观察:

例如,Animal -> CatCat -> CatAnimal -> Animal 类型的函数可以在任何需要 Cat -> Animal 的地方使用。

换句话说,→类型构造函数在参数(输入)类型中是逆变的,在返回(输出)类型中是协变的。

【讨论】:

  • 没错,忘记所有的类抽象,现在只考虑函数。不可变的MappingF[K] -> V。原因类型参数作为输入类型是 contra,类型参数作为输出类型是 co,Mapping 可以 contra 超过 K 和 co over VMutableMappingF[K] -> V(获取)和 F[K, V] -> None(设置)。 K 在这两种情况下仍然作为输入,但是因为V 既作为输入又作为输出出现,它需要具有不变性。所以MutableMappingK 相反,但在V 上是不变的。
  • @DeusXMachina 总结:在你的脑海中迭代一个类的所有函数。如果类型参数仅作为输入出现,则它可以具有逆变性。如果仅作为输出,则为协方差。如果两者都(并且其中一个函数影响另一个函数的结果,例如设置如何影响上面示例中的获取),则必须是不变的。这是一种淡化的思考方式,但它可以让你完成 99% 的工作。
  • 我想再澄清一点:我之所以说淡化是因为最后一部分是关于“一个功能会影响另一个功能的结果”。如果地图公开了一个包含方法,例如F[V] -> bool,这是否意味着V 作为此处的输入可能会强制不变,如果它是其他地方的输出?不一定,因为 contains 方法不会影响基础数据。当您分析功能之间的所有依赖关系/影响关系时,它会变得复杂。
  • @DeusXMachina 是的,请参阅pastebin.com/f1CAu5uC,如果K 不是不变的,则会进行类型检查,但会出错。另请查看github.com/python/typeshed/blob/…,您可以在其中看到MutableMapping 声明了KV 不变量(与略高于上面的Mapping 相比,后者具有V 协变量)。
  • 另见这个问题(虽然用 Java 编写,但仍然有用):stackoverflow.com/questions/2723397/…,接受答案的第一段就像我上面的中间评论。 Java 中的extends 是协变,super 是逆变。
猜你喜欢
  • 2020-11-21
  • 1970-01-01
  • 2021-12-21
  • 2022-01-12
  • 1970-01-01
  • 2017-10-10
  • 2017-07-03
  • 2021-12-31
  • 2019-06-18
相关资源
最近更新 更多