不幸的是,您的第一个示例在法律上是不安全的——它违反了被称为“Liskov 替换原则”的东西。
为了演示为什么会出现这种情况,让我稍微简化一下您的示例:我将让基类接受任何类型的object,并让子派生类接受@ 987654323@。我还添加了一点运行时逻辑:Base 类只是打印出参数; Derived 类添加了反对某些任意 int 的参数。
class Base:
def fun(self, a: object) -> None:
print("Inside Base", a)
class Derived(Base):
def fun(self, a: int) -> None:
print("Inside Derived", a + 10)
从表面上看,这似乎很好。会出什么问题?
好吧,假设我们编写以下 sn-p。这段代码的 sn-p 实际上类型检查非常好:Derived 是 Base 的子类,因此我们可以将 Derived 的实例传递给任何接受 Base 实例的程序。同样,Base.fun 可以接受任何对象,那么传递字符串肯定是安全的吗?
def accepts_base(b: Base) -> None:
b.fun("hello!")
accepts_base(Base())
accepts_base(Derived())
您也许可以看到这是怎么回事——这个程序实际上是不安全的,并且会在运行时崩溃!具体来说,最后一行被破坏了:我们传入了一个 Derived 的实例,而 Derived 的 fun 方法只接受整数。然后它会尝试将接收到的字符串与 10 相加,并立即崩溃并出现 TypeError。
这就是为什么 mypy 禁止您在您正在覆盖的方法中缩小参数类型的原因。如果 Derived 是 Base 的子类,这意味着我们应该能够在使用 Base 的任何地方替换 Derived 的实例而不会破坏任何东西。该规则特别称为 Liskov 替换原则。
缩小参数类型可以防止这种情况发生。
(请注意,mypy 要求您尊重 Liskov 的事实实际上是非常标准的。几乎所有带有子类型的静态类型语言都做同样的事情——Java、C#、C++……唯一的反例我知道是埃菲尔。)
我们可能会在您的原始示例中遇到类似问题。为了使这一点更明显,让我重命名您的一些类以更真实一点。假设我们正在尝试编写某种 SQL 执行引擎,并编写如下所示的内容:
from typing import NewType
class BaseSQLExecutor:
def execute(self, query: str) -> None: ...
SanitizedSQLQuery = NewType('SanitizedSQLQuery', str)
class PostgresSQLExecutor:
def execute(self, query: SanitizedSQLQuery) -> None: ...
请注意,此代码与您的原始示例相同!唯一不同的是名字。
我们可以再次遇到类似的运行时问题——假设我们像这样使用上述类:
def run_query(executor: BaseSQLExecutor, query: str) -> None:
executor.execute(query)
run_query(PostgresSQLExecutor, "my nasty unescaped and dangerous string")
如果允许进行类型检查,我们在代码中引入了一个潜在的安全漏洞! PostgresSQLExecutor 只能接受我们明确决定标记为“SanitizedSQLQuery”类型的不变量。
现在,解决您的另一个问题:如果我们让 Base 改为接受 Any 类型的参数,为什么 mypy 会停止抱怨?
嗯,这是因为 Any 类型具有非常特殊的含义:它表示 100% 完全动态的类型。当你说“变量 X 是 Any 类型”时,你实际上是在说“我不希望你对这个变量进行任何假设”——我希望能够使用这种类型我要你不抱怨!”
实际上将 Any 称为“可能的最广泛类型”是不准确的。实际上,它同时是最广泛的类型和最窄的类型。每个单一类型都是 Any 的子类型,并且 Any 是所有其他类型的子类型。 Mypy 将始终选择不会导致类型检查错误的任何立场。
本质上,它是一个逃生舱口,一种告诉类型检查器“我知道得更好”的方式。每当你给一个变量类型 Any 时,你实际上是完全选择不对该变量进行任何类型检查,无论好坏。
有关更多信息,请参阅typing.Any vs object?。
最后,你能做些什么呢?
好吧,不幸的是,我不确定解决这个问题的方法一定很简单:您将不得不重新设计您的代码。这从根本上讲是不合理的,而且没有任何技巧可以保证让您摆脱困境。
具体如何执行此操作取决于您要执行的操作。正如一位用户建议的那样,也许您可以使用泛型做一些事情。或者,也许您可以按照另一种建议的方式重命名其中一种方法。或者,您可以修改 Base.fun 使其使用与 Derived.fun 相同的类型,反之亦然;你可以让 Derived 不再从 Base 继承。这完全取决于您的具体情况的详细信息。
当然,如果情况真的难以处理,您可以完全放弃在该代码库的那个角落进行类型检查,并让 Base.fun(...) 接受 Any(并且接受您可能会开始遇到运行时错误)。
不得不考虑这些问题并重新设计您的代码可能看起来很麻烦——但是,我个人认为这是值得庆祝的事情! Mypy 成功地防止了您在代码中意外引入错误,并推动您编写更健壮的代码。