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 检查 SomeType
是 str
的子类型(因为我们声明 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 成功地防止了您不小心将错误引入代码,并推动您编写更健壮的代码。
看示例代码(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 检查 SomeType
是 str
的子类型(因为我们声明 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 成功地防止了您不小心将错误引入代码,并推动您编写更健壮的代码。