mypy - 可使用派生类调用给出错误

问题描述

class BaseClass:
    p: int

class DerivedClass(BaseClass):
    q: int

def p(q: Callable[[BaseClass],str]) -> None:
    return None

def r(derived: DerivedClass) -> str:
    return ""

p(r)

预期行为:

- mypy 没有错误-

实际行为:

Argument 1 to "p" has incompatible type "Callable[[DerivedClass],str]"; 
    expected "Callable[[BaseClass],str]"

解决方法

让我们谈谈type variance。在典型的子类型规则下,如果我们有一个类型 DerivedClass 是类型 BaseClass 的子类型,那么 DerivedClass 的每个实例都是 BaseClass 的实例。够简单了吧?但是现在当我们有泛型类型参数时,复杂性就出现了。

假设我们有一个获取值并返回它的类。我不知道它是怎么得到的;也许它查询一个数据库,也许它读取文件系统,也许它只是组成一个。但它有一个值。

class Getter:
    def get_value(self):
        # Some deep magic ...

现在让我们假设,当我们构造 Getter 时,我们知道它应该在编译时查询什么类型。我们可以使用类型变量对此进行注释。

T = TypeVar("T")
class Getter(Generic[T]):
    def get_value(self) -> T:
        ...

现在,Getter 是有效的。我们可以有一个 Getter[int] 获取整数和一个 Getter[str] 获取字符串。

但是这里有一个问题。如果我有一个 Getter[int],它是一个有效的 Getter[object] 吗?当然,如果我能得到一个 int 的值,它很容易向上转换,对吗?

my_getter_int: Getter[int] = ...
my_getter_obj: Getter[object] = my_getter_int

但是 Python 不允许这样做。请看,Getter 在其类型参数中被声明为 invariant。这是一种奇特的说法,尽管 intobject 的子类型,但 Getter[int]Getter[object] 没有关系。

但是,就像我说的,他们肯定应该建立关系,对吗?嗯,是。如果你的类型只用在位置(忽略了一些细节,大致意思是它只作为方法的返回值或只读属性的类型出现),那么我们可以使它是协变的。

T_co = TypeVar("T_co",covariant=True)
class Getter(Generic[T_co]):
    def get_value(self) -> T_co:
        ...

按照惯例,在 Python 中,我们使用以 _co 结尾的名称来表示协变类型参数。但实际上使它在这里成为协变的是 covariant=True 关键字参数。

现在,对于这个版本的 GetterGetter[int] 实际上是 Getter[object] 的子类型。一般来说,如果 AB 的子类型,那么 Getter[A]Getter[B] 的子类型。协方差保留子类型。

好的,这就是协方差。现在考虑相反的情况。假设我们有一个 setter 可以在数据库中设置一些值。

class Setter:
    def set_value(self,value):
        ...

与以前相同的假设。假设我们事先知道类型是什么。现在我们写

T = TypeVar("T")
class Setter:
    def set_value(self,value: T) -> None:
        ...

好的,太好了。现在,如果我有一个值 my_setter : Setter[int],那是 Setter[object] 吗?好吧,my_setter 总是可以接受一个整数值,而 Setter[object] 保证能够接受任何对象。 my_setter 不能保证,所以它实际上不是。如果我们在这个例子中尝试使 T 协变,我们会得到

error: Cannot use a covariant type variable as a parameter

因为这实际上不是有效的关系。事实上,在这种情况下,我们得到了相反的关系。如果我们有一个 my_setter : Setter[object],那么我们就可以保证我们可以向它传递任何对象,所以我们当然可以向它传递一个整数,因此我们有一个 Setter[int]。这称为逆变

T_contra = TypeVar("T_contra",contravariant=True)
class Setter:
    def set_value(self,value: T_contra) -> None:
        ...

如果我们的类型只出现在位置,我们可以使我们的类型逆变,这(再次,过度简化一点)通常意味着它作为函数的参数出现,而不是作为返回值出现。现在,Setter[object]Setter[int] 的子类型。是倒退。一般来说,如果 AB 的子类型,那么 Setter[B]Setter[A] 的子类型。逆变反转子类型关系。

现在,回到你的例子。您有一个 Callable[[DerivedClass],str] 并且想知道它是否是一个有效的 Callable[[BaseClass],str]

应用我们之前的原则,我们有一个类型 Callable[[T],S](为了简单起见,我假设只有一个参数,但实际上这在 Python 中适用于任意数量的参数)并且想询问是否 {{ 1}} 和 T 是协变的、逆变的或不变的。

那么,什么是 S?这是一个函数。我们可以做一件事:用 Callable 调用它并获得 T。所以很明显 S 仅用作参数,而 T 作为结果。只用作参数的东西是逆变的,用作结果的东西是协变的,所以实际上这样写更正确

S

Callable[[T_contra],S_co] 的参数是逆变的,这意味着如果 CallableDerivedClass 的子类型,那么 BaseClassCallable[[BaseClass],str] 的子类型,相反与你建议的关系。您需要一个可以接受any Callable[[DerivedClass],str] 的函数。带有 BaseClass 参数的函数就足够了,带有 BaseClass 参数的函数或作为 object 超类型的任何类型也是如此,但是子类型是不够的,因为它们太特定于您的合同。

,

MyPy 对象以 p 作为参数调用 r,因为仅给定类型签名,无法确定不会使用非 {{1} 调用该函数}} 实例。

例如,给定相同的类型注解,DerivedClass 可以这样实现:

p

如果 def p(q: Callable[[BaseClass],str]) -> None: obj = BaseClass() q(obj) 的实现依赖于其参数的派生属性,这将破坏 p(r)

r