mypy:方法的参数与超类型不兼容

mypy: argument of method incompatible with supertype

看示例代码(mypy_test.py):

import typing

class Base:
   def fun(self, a: str):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

现在 mypy 抱怨:

mypy mypy_test.py  
mypy_test.py:10: error: Argument 1 of "fun" incompatible with supertype "Base"

在那种情况下,我如何使用 class 层次结构并确保类型安全?

软件版本:

mypy 0.650 Python3.7.1

我尝试了什么:

import typing

class Base:
   def fun(self, a: typing.Type[str]):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

但是没有用。

一位用户评论:"Looks like you cannot narrow down accepted types in overridden method?"

但在那种情况下,如果我在基础 class 签名中使用尽可能广泛的类型 (typing.Any),它也不应该起作用。但确实如此:

import typing

class Base:
   def fun(self, a: typing.Any):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
   def fun(self, a: SomeType):
       pass

上面的代码没有来自 mypy 的抱怨。

使用通用 class 如下:

from typing import Generic
from typing import NewType
from typing import TypeVar


BoundedStr = TypeVar('BoundedStr', bound=str)


class Base(Generic[BoundedStr]):
    def fun(self, a: BoundedStr) -> None:
        pass


SomeType = NewType('SomeType', str)


class Derived(Base[SomeType]):
    def fun(self, a: SomeType) -> None:
        pass

我们的想法是定义一个具有通用类型的基础 class。现在您希望此泛型类型成为 str 的子类型,因此需要 bound=str 指令。

然后你定义你的类型 SomeType,当你 subclass Base 时,你指定泛型类型变量是什么:在本例中它是 SomeType。然后 mypy 检查 SomeTypestr 的子类型(因为我们声明 BoundedStr 必须受 str 限制),在这种情况下 mypy 很高兴。

当然,如果您定义 SomeType = NewType('SomeType', int) 并将其用作 Base 的类型变量,或者更一般地说,如果您使用 subclass Base[SomeTypeVariable],mypy 会报错如果 SomeTypeVariable 不是 str 的子类型。

我在评论中看到你想放弃 mypy。不!相反,学习类型是如何工作的;当您感觉 mypy 对您不利时,很可能是您不太了解某些事情。在这种情况下寻求其他人的帮助而不是放弃!

两个函数同名,所以只需重命名其中一个函数即可。如果你这样做,mypy 会给你同样的错误:

class Derived(Base):
        def fun(self, a: int):

将 fun 重命名为 fun1 解决了 mypy 的问题,尽管这只是 mypy 的一种解决方法。

class Base:
   def fun1(self, a: str):
       pass

不幸的是,您的第一个示例在法律上是不安全的——它违反了所谓的 "Liskov substitution principle"。

为了证明为什么是这种情况,让我稍微简化一下你的例子:我会让基数class接受任何类型的object 并让 child 派生 class 接受 int。我还添加了一些 运行 时间逻辑:Base class 只是打印出参数; Derived class 添加了针对某个任意 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)

从表面上看,这似乎很好。会出什么问题?

好吧,假设我们写了下面的片段。这段代码实际上可以很好地进行类型检查:Derived 是 Base 的子 class,因此我们可以将 Derived 的实例传递给任何接受 Base 实例的程序。同样,Base.fun 可以接受任何 object,那么传入一个字符串应该是安全的吧?

def accepts_base(b: Base) -> None:
    b.fun("hello!")

accepts_base(Base())
accepts_base(Derived())

您也许能看出这是怎么回事——这个程序实际上是不安全的,会在 运行 时崩溃!具体来说,最后一行被打破了:我们传入了一个 Derived 的实例,而 Derived 的 fun 方法只接受整数。然后它将尝试将接收到的字符串与 10 相加,并立即因 TypeError 而崩溃。

这就是为什么 mypy 禁止您缩小要覆盖的方法中的参数类型的原因。如果 Derived 是 Base 的子class,这意味着我们应该能够在我们使用 Base 的任何地方替换 Derived 的实例而不会破坏任何东西。这条规则被称为 Liskov 替换原则。

缩小参数类型可以防止这种情况发生。

(请注意,mypy 要求您尊重 Liskov 的事实实际上是非常标准的。几乎所有具有子类型的 statically-typed 语言都做同样的事情——Java、C#、C++ ...我唯一知道的 counter-example 是埃菲尔铁塔。)


我们可能会 运行 遇到与您的原始示例类似的问题。为了使这一点更加明显,让我将您的一些 classes 重命名为更真实一些。假设我们正在尝试编写某种 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: ...

请注意,此代码与您的原始示例相同!唯一不同的是名字。

我们可以再次 运行 进入类似的 运行 时间问题——假设我们像这样使用上面的 classes:

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% 完全动态的类型。当你说 "variable X is of type Any" 时,你实际上是在说 "I don't want you to assume anything about this variable -- and I want to be able to use this type however I want without you complaining!"

实际上将 Any 称为 "broadest type possible" 是不准确的。实际上,它同时是最广泛的类型和最窄的类型。每个类型都是 Any 的子类型,并且 Any 是所有其他类型的子类型。 Mypy 将始终选择不会导致类型检查错误的立场。

本质上,它是一个逃生舱口,一种告诉类型检查器的方式 "I know better"。每当你给一个变量类型 Any 时,你实际上完全选择退出该变量的任何 type-checking,无论好坏。

有关详细信息,请参阅


最后,对于这一切你能做些什么?

嗯,不幸的是,我不确定解决这个问题的简单方法:您将不得不重新设计代码。它从根本上说是不可靠的,并且没有任何技巧可以保证让您摆脱困境。

具体如何执行此操作取决于您要执行的操作。正如一位用户所建议的,也许您可​​以使用泛型做些事情。或者,也许您可​​以按照另一种建议重命名其中一种方法。或者,您可以修改 Base.fun,使其使用与 Derived.fun 或 vice-versa 相同的类型;您可以使 Derived 不再继承自 Base。这完全取决于您具体情况的细节。

当然,如果情况确实 棘手的,您可以完全放弃该代码库那个角落的 type-checking 并使 Base.fun (...) 接受任何(并接受你可能会 运行 进入 运行 时间错误)。

必须考虑这些问题并重新设计您的代码似乎是一件很麻烦的事情——但是,我个人认为这是值得庆祝的事情! Mypy 成功地防止了您不小心将错误引入代码,并推动您编写更健壮的代码。