【问题标题】:mypy: argument of method incompatible with supertypemypy:方法的参数与超类型不兼容
【发布时间】:2019-06-18 04:38:05
【问题描述】:

看示例代码(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 的抱怨。

【问题讨论】:

  • 看起来您无法缩小覆盖方法中接受的类型?

标签: python python-3.x type-hinting mypy


【解决方案1】:

如下使用泛型类:

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 不是str 的子类型,mypy 会报错。

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

【讨论】:

    【解决方案2】:

    两个函数具有相同的名称,因此只需重命名其中的 1 个函数。如果你这样做,mypy 会给你同样的错误:

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

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

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

    【讨论】:

      【解决方案3】:

      不幸的是,您的第一个示例在法律上是不安全的——它违反了被称为“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 成功地防止了您在代码中意外引入错误,并推动您编写更健壮的代码。

      【讨论】:

      • 伙计,多么可靠的答案!
      • 派生输出类型呢?为什么缩小输出类型是一个问题或不安全?
      • @dba -- 缩小派生输出类型实际上是安全的!完整的规则是缩小派生输入类型是不安全的,而加宽派生输出类型是不安全的。 (或者使用更正式的语言,函数的输入类型是逆变,而输出类型是协变。)例如,如果父输出类型是int ,将派生输出类型扩大到object 是不安全的。我的回答只讨论了输入类型的规则,因为这是 OP 在他们的问题中或多或少要问的问题。
      猜你喜欢
      • 1970-01-01
      • 2021-09-26
      • 2022-06-14
      • 1970-01-01
      • 2018-09-07
      • 2021-07-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多