Mypy 在 __init__ 覆盖中接受不兼容的类型
Mypy accepts an incompatible type in __init__ override
我有以下 Foo
基础 class,以及从它继承的 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 returns 一个错误——这当然是我所期望的。
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
替换为 [=17],像 foo = Foo(12)
这样的代码不会进行类型检查=].
为什么 Mypy 接受我用不兼容的类型覆盖 __init__
? __init__
是否与其他方法区别对待?
另外,Mypy 这样做对吗?我是否正确认为最后一个 Bar
class 违反了 LSP?
注意我真的不太确定。我让投票评判。
tl;dr LSP 不适用于 __init__
,因为它不能用作实例上的方法。
Subtype Requirement: Let Q(t) be a property provable about objects t of type T. Then Q(s) should be true for objects s of type S where S is a subtype of T.
对于do
,我们有
Q(x) = signature of x.do
t = Foo(...)
T = Foo
s = Bar(...)
S = Bar
并且由于 do
对 Foo
的所有实例具有相同的签名(即它是 T 的对象 t 的可证明 属性),这对于对象 t 也必须为真输入 Bar
。但是,对于 __init__
,它实际上是 class 上的一个方法,我们有
t = Foo
T = type[Foo]
s = Bar
S = type[Bar]
在这种情况下,关于 __init__
类型 type[Foo]
的对象 t(即 classes)没有可证明的 属性,因为这包括 Foo
和 Bar
,因此 LSP 对 Bar
.
的签名只字未提
一般认为里氏替换原则不适用于构造方法。如果我们将构造函数方法视为对象接口的一部分,那么在许多情况下,继承系统将变得极其难以管理,并导致一大堆其他的复杂情况。请参阅 this question 我前阵子在软件工程上发表的文章。
然而,情况有点复杂,因为 __init__
并不是 真正的构造函数方法(应该是 __new__
)—它是一个初始化方法,可以在同一个实例上多次调用。初始化方法几乎总是与构造方法具有相同的签名只是“碰巧”。
由于 __init__
可以在同一个实例上多次调用,就像被视为对象接口一部分的“普通”方法一样,目前 active discussion核心开发人员关于 __init__
方法 是否应该 在某些方面被视为对象接口的一部分。
总结: ¯\_(ツ)_/¯
Python 是一种非常 的动态语言,这意味着对其类型系统的推理通常会有些奇怪。
我有以下 Foo
基础 class,以及从它继承的 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 returns 一个错误——这当然是我所期望的。
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
替换为 [=17],像 foo = Foo(12)
这样的代码不会进行类型检查=].
为什么 Mypy 接受我用不兼容的类型覆盖 __init__
? __init__
是否与其他方法区别对待?
另外,Mypy 这样做对吗?我是否正确认为最后一个 Bar
class 违反了 LSP?
注意我真的不太确定。我让投票评判。
tl;dr LSP 不适用于 __init__
,因为它不能用作实例上的方法。
Subtype Requirement: Let Q(t) be a property provable about objects t of type T. Then Q(s) should be true for objects s of type S where S is a subtype of T.
对于do
,我们有
Q(x) = signature of x.do
t = Foo(...)
T = Foo
s = Bar(...)
S = Bar
并且由于 do
对 Foo
的所有实例具有相同的签名(即它是 T 的对象 t 的可证明 属性),这对于对象 t 也必须为真输入 Bar
。但是,对于 __init__
,它实际上是 class 上的一个方法,我们有
t = Foo
T = type[Foo]
s = Bar
S = type[Bar]
在这种情况下,关于 __init__
类型 type[Foo]
的对象 t(即 classes)没有可证明的 属性,因为这包括 Foo
和 Bar
,因此 LSP 对 Bar
.
一般认为里氏替换原则不适用于构造方法。如果我们将构造函数方法视为对象接口的一部分,那么在许多情况下,继承系统将变得极其难以管理,并导致一大堆其他的复杂情况。请参阅 this question 我前阵子在软件工程上发表的文章。
然而,情况有点复杂,因为 __init__
并不是 真正的构造函数方法(应该是 __new__
)—它是一个初始化方法,可以在同一个实例上多次调用。初始化方法几乎总是与构造方法具有相同的签名只是“碰巧”。
由于 __init__
可以在同一个实例上多次调用,就像被视为对象接口一部分的“普通”方法一样,目前 active discussion核心开发人员关于 __init__
方法 是否应该 在某些方面被视为对象接口的一部分。
总结: ¯\_(ツ)_/¯
Python 是一种非常 的动态语言,这意味着对其类型系统的推理通常会有些奇怪。