gpt4 book ai didi

python - mypy:方法的参数与父类(super class)型不兼容

转载 作者:太空宇宙 更新时间:2023-11-03 10:49:12 24 4
gpt4 key购买 nike

查看示例代码(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对上面的代码没有任何提示。

最佳答案

不幸的是,您的第一个示例在法律上不安全-违反了被称为“Liskov替换原理”的内容。

为了演示为什么会发生这种情况,让我稍微简化一下示例:我将让基类接受任何类型的object,并让子派生类接受int。我还添加了一些运行时逻辑:基类仅打印出参数。 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)

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

好吧,假设我们编写以下代码段。这个代码片段实际上可以很好地进行类型检查: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成功地阻止了您向代码中意外引入错误,并促使您编写更强大的代码。

关于python - mypy:方法的参数与父类(super class)型不兼容,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54346721/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com