Generics in python protocols – 协变性和逆变性

huangapple go评论130阅读模式
英文:

Generics in python protocols - Covariance and Contravariance

问题

在阅读了关于Python中协变和逆变的内容后,我仍然发现自己难以理解为什么不变类型必须被转换为逆变类型,才能在协议的上下文中使用。我希望有人能进一步解释这个概念给我。例如,让我们假设以下情况:

from typing import Literal, Protocol, TypeVar

MyType = Literal["literal_1"]

G = TypeVar("G")

class MyProtocol(
    Protocol[G],
):
    @staticmethod
    def do_work(message: G):
        raise NotImplementedError

class CustomClass(
    MyProtocol[MyType]
):
    @staticmethod
    def do_work(message: MyType):
        pass

literal_1: MyType = "literal_1"

CustomClass.do_work(literal_1)

这将导致使用pyright/mypy时出现以下错误:

警告:在通用协议“MyProtocol”中使用类型变量“G”应该是逆变的(reportInvalidTypeVarUse)

将函数更改为返回相同类型的通用类型:

def do_work(message: G) -> G:
    raise NotImplementedError

@staticmethod
def do_work(message: MyType) -> Mytype:
    return message

这个错误就消失了。

我已经阅读了多个来源,它们都会以不同的方式解释以下内容:

简短的解释是,你的方法破坏了子类型的传递性;请查看PEP 544的这一部分,以获取更多信息。

PEP 544的相关部分

我已经阅读了这一部分,但仍然对为什么会为这个特定示例引发错误感到困惑。另外,当协议中的函数有返回类型时,我也不明白为什么需要协变。

英文:

after having read up on covariance and contravariance within python I still find myself struggling to understand why an Invariant has to be made a contravariant to be used within the context of a protocol and I was hoping someone could further explain this concept to me. For example.

Let's assume the following:

from typing import Literal, Protocol, TypeVar


MyType = Literal["literal_1"]

G = TypeVar("G")

class MyProtocol(
    Protocol[
        G
    ],
):
    @staticmethod
    def do_work(message: G):
        raise NotImplementedError


class CustomClass(
    MyProtocol[
        MyType
    ]
):
    @staticmethod
    def do_work(message: MyType):
        pass

literal_1: MyType = "literal_1"

CustomClass.do_work(literal_1)

This will yield The following error using pyright/mypy:

warning: Type variable "G" used in generic protocol "MyProtocol" should be contravariant (reportInvalidTypeVarUse) 

Changing the function to return a Generic of the same type:

def do_work(message: G) -> G:
    raise NotImplementedError

@staticmethod
def do_work(message: MyType) -> Mytype:
    return message

This error disappears.

I have read multiple sources that will paraphrase the following:

The short explanation is that your approach breaks subtype transitivity; see this section of PEP 544 for more information.

https://www.python.org/dev/peps/pep-0544/#overriding-inferred-variance-of-protocol-classes

I have read the section and am still confused as to why this error is being thrown for this particular example. Additionally I am confused as to why covariance is needed when a return type is given for a function defined in a protocol.

答案1

得分: 1

想象一下,如果你有一个BaseDerived类,并且有一个符合MyProtocol[Base]的对象。你应该允许将该对象传递给一个期望MyProtocol[Derived]的函数,因为在该函数中,它们将调用the_object.do_work(Derived()),这是可以传递给期望Base的版本的有效操作,因为DerivedBase的子类。因此,更通用的MyProtocol[Base]在更特定的MyProtocol[Derived]有效的地方也是有效的,因此类型参数是逆变的(应该被指定为逆变的)。

通常作为函数参数的类型参数应该是逆变的,因为具有更一般的参数类型的函数总是可以用更具体的参数类型调用。

另一方面,返回类型中的类型参数通常是协变的,因为通常情况下,返回Base的函数不能通用地用于需要返回Derived的地方,但反之却成立:始终可以在需要返回Base的地方使用返回Derived的函数。

因此,当你将签名更改为def do_work(message: G) -> G:时,现在G既是参数类型又是返回类型,所以它应该是协变和逆变的,但它也不能是任何一个;因此,不变的默认值是正确的。

英文:

Imagine if you had a Base and Derived class, and an object conforming to MyProtocol[Base]. You should be allowed to pass that object to a function expecting a MyProtocol[Derived], since in that function, they would call the_object.do_work(Derived()), which is a valid thing to pass to the version that expects a Base, since Derived is a subclass of Base. Hence, the more generic MyProtocol[Base] is valid wherever the more specific MyProtocol[Derived] is, so the type parameter is contravariant (and should be designated as such).

Generally type parameters used as function parameters should be contravariant, since a function with more general parameter types is always callable with more specific parameter types.


On the other hand, type parameters in the return type are generally covariant, since a function returning Base is not generally usable where you need a function returning Derived, but the opposite is true: a function returning Derived can always be used where you need a function that returns Base.

Therefore, when you changed your signature to def do_work(message: G) -> G:, now you have G as both a parameter and a return type, so it should be both covariant and contravariant, but it also can't be either one; hence the default of invariant is correct.

huangapple
  • 本文由 发表于 2023年3月9日 17:55:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/75682936.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定