查看示例代码(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"
在那种情况下,我该如何使用类层次结构并保持类型安全?
软件版本:
mypy 0.650 Python 3.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
但这没有帮助。
一个用户评论道:“看起来您无法以覆盖方法缩小接受的类型吗?”
但是在那种情况下,如果我在基类签名中使用可能的最广泛的类型(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没有抱怨。
答案 0 :(得分:2)
不幸的是,您的第一个示例在法律上是不安全的-它违反了被称为“ Liskov替换原理”的内容。
为演示为什么为什么,让我简化一下您的示例:我将让基类接受任何类型的object
并让子派生类接受int
。我还添加了一些运行时逻辑:基类仅打印出参数。派生类添加了针对任意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的子类,因此我们可以将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相同的类型,反之亦然;您可以使“派生”不再从Base继承。这完全取决于您的具体情况。
当然,如果真正的情况是 ,您可以完全放弃该代码库那一角的类型检查,并让Base.fun(...)接受Any(并且接受您可能会开始遇到运行时错误)。
不得不考虑这些问题并重新设计代码似乎很麻烦,但是我个人认为这值得庆祝! Mypy成功地阻止了您意外地将错误引入代码中,并促使您编写更强大的代码。
答案 1 :(得分:0)
使用如下通用类:
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
这个想法是用一个通用类型定义一个基类。现在,您希望此通用类型是str
的子类型,因此是bound=str
指令。
然后定义类型SomeType
,并在子类Base
时指定泛型类型变量:在这种情况下为SomeType
。然后mypy检查SomeType
是str
的子类型(因为我们说过BoundedStr
必须以str
为界),在这种情况下,mypy很高兴。>
当然,如果您定义SomeType = NewType('SomeType', int)
并将其用作Base
的类型变量,或者更普遍地,如果Base[SomeTypeVariable]
是SomeTypeVariable
的子类,mypy将会抱怨不是str
的子类型。
我在一条评论中读到您想抛弃mypy。别!相反,要学习类型如何工作。当您感到Mypy不利于您时,很有可能您不太了解某些内容。在这种情况下,请寻求他人的帮助而不是放弃!
答案 2 :(得分:0)
这两个函数具有相同的名称,因此只需重命名其中一个函数即可。如果执行此操作,mypy将给您同样的错误:
class Derived(Base):
def fun(self, a: int):
将fun重命名为fun1可以解决mypy的问题,尽管这只是mypy的一种解决方法。
class Base:
def fun1(self, a: str):
pass