使用Mypy与类继承的困惑--List与Sequence的区别

问题描述 投票:0回答:1

请原谅我的困惑--我是个使用打字机的新手,并试图将它与以下内容一起使用 mypy 为检查.看来我的问题question似乎发生在人们开始使用打字和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 是有问题的,当你进行子类化时。 ....或者可能搞不清楚为什么 "List是不变的,但Sequence是共变的"。

所以我想问一下,也许是代表其他像我这样试图真正使用类型化的人,所以Mypy,为了更多琐碎的例子--是否有任何关于List有问题的原因的好解释,以及一些例子?

python mypy
1个回答
1
投票

假设我们在你的原始代码中加入以下内容。

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)

这个程序从表面上看应该进行类型检查 specific 是TestSeries的一个实例,所以它是合法的。modify_series(specific). 同样地: specific.imgs 是List[SpecificImageType1]类型的,所以在做 check_specific_images(specific.imgs) 也是合法的。

然而,如果我们真正尝试运行这个程序,当我们调用 check_specific_images! The modify_series 在我们的List[SpecificImageType1]中添加了一个TestResultImage对象,导致后续调用了 check_specific_images 在运行时崩溃。

这个问题从根本上讲是mypy(或者几乎所有其他正常的类型系统)不会让List[SpecificImageType1]被当作List[TestResultImage]的子类型的原因。为了让一个类型成为另一个类型的有效子类型,应该可以在任何期望父类型的位置安全地使用子类型。对于列表来说,这根本不是真的。

为什么这么说呢?因为列表支持 写操作. 在List[TestResultImage]中插入一个TestResultImage(或TestResultImage的任何子类型)应该总是安全的,而对于List[SpecificImageType1]来说,这并不是真的。


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

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


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

这就提出了一个问题:Wrapper[Parent]与Wrapper[Child]的关系如何?

对此,有四种可能的答案。

  • Wrapper是 共变: Wrapper[Child]是Wrapper[Parent]的一个子类型。

  • Wrapper是 违变: Wrapper[Parent]是Wrapper[Child]的一个子类型。

  • Wrapper是 不变性: Wrapper[Parent]和Wrapper[Child]互不相关,都不是对方的子类型。

  • Wrapper是 双变量: Wrapper[Parent]是Wrapper[Child]的一个子类型。 Wrapper[Child]是Wrapper[Parent]的一个子类型。

当你定义Wrapper[T]时,mypy会让你选择你想要的类型是共变、反变还是不变。一旦你做出了选择,mypy就会执行以下规则。

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

Mypy不允许你创建双变量类型:只有当这样的类型是安全的时候,如果它支持 既不 也不对T进行读写操作--这将是非常没有意义的。

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

这里的高级直觉是,支持对T的读操作或写操作都会对Wrapper[Parent]与Wrapper[Child]的关系进行约束--如果你同时支持这两种操作,综合约束最终会使这两种类型根本不相关。

© www.soinside.com 2019 - 2024. All rights reserved.