将Mypy与类继承一起使用的困惑-列表与序列

  • 本文关键字:列表 Mypy 继承 一起 python mypy
  • 更新时间 :
  • 英文 :


请原谅我的困惑-我是打字的新手,并尝试将其与mypy一起用于检查。我遇到的问题似乎经常发生在开始使用打字和Mypy的人身上。

问题

我试图定义一个数据类的抽象组合,它将被子类化为具体的类,以添加额外的数据。

因此,在一个简化的形式中,我试图做以下事情:

from dataclasses import dataclass
from typing import List
@dataclass
class TestResultImage:
base_var_a: int 

@dataclass
class TestSeries:
imgs: List[TestResultImage]
# --- concrete instances -------
@dataclass
class SpecificImageType1(TestResultImage):
specific_var_b: float
specific_var_c: int 

@dataclass
class SpecificSeries(TestSeries):
imgs: List[SpecificImageType1]

Mypy在上面失败,出现错误\

error: Incompatible types in assignment (expression has type "List[SpecificImageType1]", base class "TestSeries" defined the type as "List[TestResultImage]")
note: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance
note: Consider using "Sequence" instead, which is covariant

修复

将{List}更改为{Sequence}解决了问题,如错误中所述。

问题

我见过不少SO和Mypy git的问题都与这个问题和人们的困惑有关。

所以我去尝试阅读尽可能多的Mypy文档。

但是,当您进行子类化时,为什么List会有问题,这仍然是非常令人困惑的。。。或者可能混淆了为什么"列表是不变的,但序列是协变的"。

因此,我想问,也许是代表像我这样试图真正使用打字的人,比如Mypy,对于不仅仅是琐碎的例子,有没有什么好的解释可以解释List有问题的原因,以及任何地方的一些例子?

假设我们将以下内容添加到原始代码中:

def check_specific_images(imgs: List[SpecificImageType1]) -> None:
for img in imgs:
print(img.specific_var_b)
def modify_series(series: TestSeries) -> None:
series.append(TestResultImage(1))
specific = SpecificTestSeries(imgs=[
SpecificImageType1(1, 2.0, 3),
SpecificImageType1(4, 5.0, 6),
])
modify_series(specific)
check_specific_images(specific.imgs)

这个程序表面上应该键入check:specific是TestSeries的一个实例,所以执行modify_series(specific)是合法的。类似地,specific.imgs的类型为List[SpecificImageType1],因此执行check_specific_images(specific.imgs)也是合法的。

然而,如果我们真的尝试运行这个程序,当我们调用check_specific_images时,我们会得到一个运行时错误!modify_series向我们的List[SpecificImageType1]添加了一个TestResultImage对象,导致随后对check_specific_images的调用在运行时崩溃。

这个问题从根本上解释了为什么mypy(或几乎任何其他健全的类型系统)不会让List[SpecificImageType1]被视为List[TestResultImage]的子类型。为了使一个类型成为另一个类型的有效子类型,应该可以在任何需要父类型的位置安全地使用该子类型。对于列表来说,这根本不是真的。

为什么?因为列表支持写入操作。将TestResultImage(或TestResultImage的任何子类型)插入List[TestResultImage]应该始终是安全的,而List[SpecificImageType1]则不是这样。


所以,如果问题是列表是可变的,那么如果我们使用一个不可变的类型——只支持读取操作呢?这将使我们完全回避这个问题。

这就是Sequence:它是一个包含列表支持的所有只读方法的类型(并且是List的超类型)。


更广泛地说,让我们假设我们有某种泛型类型Wrapper[T]以及两个类Parent和Child,其中Child是Parent的子类型。

这就提出了一个问题:Wrapper[PParent]和Wrapper[Child]之间有什么关系?

对此有四种可能的答案:

  • Wrapper是协变:Wrapper[Child]是Wrapper[PParent]的一个子类型。

  • Wrapper是的反变体:Wrapper[PParent]是Wrapper[Child]的一个子类型。

  • Wrapper是不变量:Wrapper[PParent]和Wrapper[Child]彼此无关,两者都不是另一个的子类型。

  • Wrapper是双变量:Wrapper[PParent]是Wrapper[Child]的子类型,Wrapper[Thild]是Wraper[PParet]的子类型。

当你定义Wrapper[T]时,mypy会让你选择你希望该类型是协变的、反变的还是不变的。一旦你做出选择,mypy将执行以下规则:

  1. 如果一个类是协变的,它只能支持对T的读取操作。在实践中,这意味着不允许定义接受任何类型T的方法
  2. 如果一个类是反变量的,它只能支持对T的写操作。实际上,这意味着不允许定义返回任何T类型的方法
  3. 如果一个类是不变的,它可以支持对T的读写操作。对可以定义的方法类型没有限制

Mypy不允许您创建双变量类型:只有当这样的类型是安全的时,它才不支持对t的读写操作,这将是毫无意义的。

在编程语言/类型系统中,你通常只看到有意使泛型尽可能简单的双变量类型,即使这意味着让用户在他们的程序中引入如上所示的错误。

这里的高级直觉是,支持对T的读操作或写操作将限制Wrapper[PParent]与Wrapper[Child]的关系——如果你支持这两种操作,那么组合的约束将使这两种类型完全无关。

最新更新