代码之家  ›  专栏  ›  技术社区  ›  anon

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

  •  4
  • anon  · 技术社区  · 5 年前

    查看示例代码(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的投诉。

    3 回复  |  直到 5 年前
        1
  •  0
  •   Michael0x2a    5 年前

    不幸的是,你的第一个例子是合法不安全的——它违反了所谓的“里斯科夫替代原则”。

    演示 为什么? 就是这样,让我简化一下您的示例:我将让基类接受任何类型的 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)
    

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

    好吧,假设我们写下面的片段。这段代码实际上检查的很好:派生的是基的子类,因此我们可以将派生的实例传递到接受基实例的任何程序中。同样地,base.fun可以接受任何对象,所以传入字符串肯定是安全的吗?

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

    您可能会看到这是去哪里-这个程序实际上是不安全的,将在运行时崩溃!具体地说,最后一行被破坏了:我们传入了派生的和派生的 fun 方法只接受int。然后,它将尝试将它接收到的字符串与10相加,并立即因类型错误而崩溃。

    这就是为什么mypy禁止您缩小覆盖方法中参数类型的范围。如果派生是基的一个子类,这意味着我们应该能够 代替 派生的实例,在我们使用基的任何位置都不会破坏任何内容。这条规则特别被称为里斯科夫替代原理。

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

    (值得注意的是,Mypy要求你尊重Liskov这一事实实际上是相当标准的。几乎所有具有子类型的静态类型语言都做同样的事情——Java、C、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只能接受我们明确决定标记为“已清理的sqlquery”类型的字符串的不变量已损坏。


    现在,来回答你的另一个问题:为什么如果我们让BASE接受任何类型的论点,Mypy就不再抱怨了?

    这是因为任何类型都有一个非常特殊的含义:它代表一个100%完全动态的类型。当你说“变量x的类型是any”时,你实际上是在说“我不希望你假定 任何东西 关于这个变量——我希望能够使用这个类型,不管你怎么抱怨!”

    事实上,称任何“可能最广泛的类型”都是不准确的。实际上,它同时是最广泛的类型和最窄的类型。每个类型都是any的子类型,any是所有其他类型的子类型。Mypy将始终选择不会导致类型检查错误的任何站姿。

    本质上,它是一个逃生舱,一种告诉类型检查器“我更了解”的方式。每当你给一个变量类型any时,实际上你完全不需要检查这个变量的类型,不管是好是坏。

    有关更多信息,请参阅 typing.Any vs object? .


    最后,你能做些什么?

    不幸的是,我不确定是否有一个简单的方法可以解决这个问题:你必须重新设计你的代码。它从根本上说是不健全的,而且没有任何技巧可以保证让你摆脱困境。

    你到底要怎么做取决于你到底想做什么。正如一位用户建议的那样,也许您可以对泛型做些什么。或者您可以按照另一个建议重命名其中一个方法。或者,您可以修改base.fun,使其使用与derived.fun相同的类型,或者反之亦然;您可以使derived不再从base继承。这完全取决于你具体情况的细节。

    当然,如果情况真的 难处理的是,您可以完全放弃在该代码基的那个角进行类型检查,并使base.fun(…)接受任何(并接受您可能开始遇到运行时错误)。

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

        2
  •  0
  •   Frans    5 年前

    两个函数都有相同的名称,所以只需重命名其中1个函数即可。如果你这样做,Mypy会给你同样的错误:

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

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

    class Base:
       def fun1(self, a: str):
           pass
    
        3
  •  0
  •   gniourf_gniourf    5 年前

    使用如下通用类:

    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 ,指定泛型类型变量是什么:在本例中,它是 基类型 . 然后我的宝贝检查那个 基类型 是一个亚型 STR (因为我们声明 BoundedStr 必须是以 STR 在这种情况下,Mypy很高兴。

    当然,如果你定义的话,Mypy会抱怨的。 SomeType = NewType('SomeType', int) 并将其用作 基地 或者,更一般地说,如果您将 Base[SomeTypeVariable] 如果 SomeTypeVariable 不是的子类型 STR .

    我在一篇评论中读到你想抛弃我。不要!相反,学习类型是如何工作的;当你在你觉得Mypy反对你的情况下,很可能有一些事情你不太明白。在这种情况下,寻求别人的帮助,而不是放弃!