【问题标题】:Mypy accepts an incompatible type in __init__ overrideMypy 在 __init__ 覆盖中接受不兼容的类型
【发布时间】:2021-11-22 10:46:30
【问题描述】:

我有以下Foo 基类和继承自它的Bar

class Foo:
    def __init__(self, x: int) -> None:
        self._x = x

    def do(self, x: int) -> None:
        pass


class Bar(Foo):
    pass

如果我在Bar 中覆盖Foo.do,并为不兼容的东西更改x 参数的类型(也就是说,不比int 更通用),那么Mypy 会返回一个错误——这当然是我的期望。

class Bar(Foo):
    def do(self, x: str) -> None:
        pass

错误:

test.py:10: error: Argument 1 of "do" is incompatible with supertype "Foo"; supertype defines the argument type as "int"
test.py:10: note: This violates the Liskov substitution principle
test.py:10: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Found 1 error in 1 file (checked 1 source file)

但是,如果我使用不兼容的参数类型覆盖 __init__,那么 Mypy 会接受它:

class Bar(Foo):
    def __init__(self, x: str) -> None:
        self._x = 12

Mypy 输出:

Success: no issues found in 1 source file

在我看来,用不兼容的类型覆盖 __init__ 也违反了 LSP,因为如果我们将 Foo 替换为 Bar,像 foo = Foo(12) 这样的代码不会进行类型检查。

为什么 Mypy 接受我用不兼容的类型覆盖 __init____init__ 的处理方式是否与其他方法不同? 另外,Mypy 这样做对吗?最后一个 Bar 类违反了 LSP,我是否正确?

【问题讨论】:

  • 请注意,如果您在Bar 中实际使用self._x = xsuper().__init__(x),则 被捕获。它是否违反 LSP 是一个有趣的问题 - class Bar 不能换成 Foo,但它们的 instances 是兼容的。

标签: python types mypy python-typing liskov-substitution-principle


【解决方案1】:

注意我真的不确定。我让投票来判断。

tl;dr LSP 不适用于 __init__,因为它不适合用作实例上的方法。

LSP states

子类型要求:令 Q(t) 是关于类型 T 的对象 t 的可证明性质。那么 Q(s) 对于类型 S 的对象 s 应该为真,其中 S 是 T 的子类型。

对于do,我们有

Q(x) = signature of x.do
t = Foo(...)
T = Foo
s = Bar(...)
S = Bar

并且由于doFoo 的所有实例具有相同的签名(即它是T 的对象t 的可证明属性),对于Bar 类型的对象也必须如此。但是,对于__init__,它实际上是类上的一个方法,我们有

t = Foo
T = type[Foo]
s = Bar
S = type[Bar]

在这种情况下,关于 __init__ 类型为 type[Foo] 的对象 t(即类)没有可证明的属性,因为这包括 both FooBar,所以 LSP 说与Bar的签名无关。

【讨论】:

    【解决方案2】:

    Liskov 替换原则通常不被认为适用于构造方法。如果我们将构造函数方法视为对象接口的一部分,它将使继承系统在许多情况下极难管理,并导致一大堆其他复杂情况。请参阅 this question 我不久前在软件工程上提出的问题。

    然而,情况有点复杂,因为__init__ 不是真正的构造方法(应该是__new__)——它是一个初始化方法,它可以是在同一个实例上多次调用。只是“碰巧”是初始化方法几乎总是与构造方法具有相同的签名。

    由于__init__ 可以在同一个实例上被多次调用,就像被视为对象接口的一部分的“普通”方法一样,目前核心开发人员中有active discussion 是否可以在同一实例上调用@987654328 @ 方法应该在某些方面被视为对象接口的一部分。

    总结: ¯\_(ツ)_/¯

    Python 是一种非常动态的语言,这意味着对其类型系统的推理往往有点奇怪。

    【讨论】:

    • scala 值得在这里考虑。构造函数以及objects 上的apply 方法,都类似于本次讨论的python 的__init__,也不受约束
    • @joel 你能多说一点/按我的方式发送一些链接吗?我知道关于 Scala 的知识,但这听起来很有趣 :)
    • object 不是很重要,但here 是抽象类的工作原理。我现在记得你不能在 scala 中扩展具体的类。在那个例子中,他们保留了签名,但你可以添加参数(就像我做的here
    • 顺便说一句“因为__init__ 可以在同一个实例上多次调用”。虽然这是真的,但你什么时候会调用它两次?
    • @joel 我实际上已经实现了一个上下文管理器,它在我目前正在处理的项目中精确地使用了这种模式。对象的__init__ 在上下文管理器的__exit____call__ 方法中都被调用。每次我输入with 语句时,__call__ 方法调用__init__ 来改变上下文管理器的内部状态,__exit__ 方法调用__init__ 将内部状态重置为“默认”设置是时候离开with 区块了。
    猜你喜欢
    • 2022-06-14
    • 2020-08-01
    • 2017-10-10
    • 1970-01-01
    • 2018-07-26
    • 2011-03-09
    • 1970-01-01
    • 2014-04-18
    • 1970-01-01
    相关资源
    最近更新 更多