Mypy 在访问超类属性时给出错误的类型

Mypy give incorrect type when accessing superclass attribute

我正在尝试设置超类属性并在子类中访问它,如下所示:

class A:
    pass

class B(A):
    pass

class C:
    def __init__(self, param: A) -> None:
     self.store = param

class D(C):
    def __init__(self, param: B) -> None:
     super().__init__(param)
     reveal_type(self.store)

当我 运行 mypy 我得到 "Revealed type is 'test.A'" 我如何让它在子类中引用 B 而不是 A?

Mypy 实际上在这里给出了正确的答案。协变可变属性无效,因此 D._store 的类型实际上是 A。这可能听起来像是一堆理论上的废话,所以我会更详细地解释它:

您声明每个 D 个实例 is-a C 个实例。 C 有一个名为 _store 的可变属性 A。因此,在任何时候将 _store 属性设置为 A 类型的任何值都是合法的。这意味着 D 实例的任何使用都必须能够处理这样一个事实,即它的 _store 可能是 A,而不是 B。这意味着 D._store 的类型是 A.

如果您的 C._store 实际上是可变的,那么这就是您代码中的真正错误。任何时候你想在 D._store 上使用 B 方法,你需要用 isinstancetry/except 进行测试,之后你当然可以安全地依赖于值是 B 的事实(即使所有者不是 D 也可以工作)。


如果,另一方面,你的 C._store 是不可变的,你想做的是有效的(尽管你真的应该使用 __new__ 而不是 __init__ 来设置它在那种情况下),但由于当前版本的 Mypy 的限制(或者更确切地说,在 Python 的静态类型系统中,它的表现力不如动态类型系统,在某些方面为了简单起见,有意地,在其他方面只是因为还没有考虑到所有事情),您不能明确指定该事实。而且我不确定它是否会在一般情况下被修复(尽管像 dataclass 不可变属性这样的特定情况可能会在某个时候被修复)。


一个选项是将属性包装在 (read-only) @propertyas documented for Protocol members 中。但是,这当然会增加您的实施开销和设计的复杂性。

或者,感谢 a bug in Mypy,您实际上可以通过对类型检查器说谎来解决这个问题:

class C:
    _store: A
    def __init__(self, param: A) -> None:
     self._store = param

class D(C):
    _store: B
    def __init__(self, param: B) -> None:
     super().__init__(param)
     reveal_type(self._store)

如果 _store 真的是不可变的,这会给你正确的答案——直到错误被修复。假设您没有不安全地滥用该错误,并且可以坚持使用当前版本的 Mypy,直到出现一个可以让您正确编写内容的版本,现在这可能没问题。

如果在那之前你不能坚持使用当前版本的 Mypy,你总是可以这样做:

class C:
    _store: A: # type: ignore
    def __init__(self, param: A) -> None:
     self._store = param

class D(C):
    _store: B
    def __init__(self, param: B) -> None:
     super().__init__(param)
     reveal_type(self._store)

但这当然意味着您根本不会在 _store 上进行任何检查;您可以在其中粘贴一个 int,它会生效。