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]"
答案 0 :(得分:2)
让我们谈谈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。这是一种奇特的说法,尽管 int
是 object
的子类型,但 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
关键字参数。
现在,对于这个版本的 Getter
,Getter[int]
实际上是 Getter[object]
的子类型。一般来说,如果 A
是 B
的子类型,那么 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]
的子类型。是倒退。一般来说,如果 A
是 B
的子类型,那么 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]
的参数是逆变的,这意味着如果 Callable
是 DerivedClass
的子类型,那么 BaseClass
是 Callable[[BaseClass], str]
的子类型,相反与你建议的关系。您需要一个可以接受any Callable[[DerivedClass], str]
的函数。带有 BaseClass
参数的函数就足够了,带有 BaseClass
参数的函数或作为 object
超类型的任何类型也是如此,但是子类型是不够的,因为它们太特定于您的合同。
答案 1 :(得分:0)
MyPy 对象以 p
作为参数调用 r
,因为仅给定类型签名,无法确定不会使用非 {{1} 调用该函数}} 实例。
例如,给定相同的类型注解,DerivedClass
可以这样实现:
p
如果 def p(q: Callable[[BaseClass], str]) -> None:
obj = BaseClass()
q(obj)
的实现依赖于其参数的派生属性,这将破坏 p(r)
:
r