ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

python – mypy:与超类型不兼容的方法的参数

2019-07-10 13:06:36  阅读:243  来源: 互联网

标签:python python-3-x typing 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"

在这种情况下,我如何使用类层次结构并且类型安全?

软件版本:

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替代原则”的东西.

为了说明为什么会这样,让我简化一下你的例子:我将让基类接受任何类型的对象并让子派生类接受一个int.我还添加了一些运行时逻辑: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)

从表面上看,这似乎完全没问题.怎么可能出错?

好吧,假设我们编写以下代码段.这段代码实际上类型检查非常好:Derived是Base的子类,因此我们可以将Derived的实例传递给任何接受Base实例的程序.同样地,Base.fun可以接受任何对象,所以传递一个字符串肯定是安全的吗?

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

accepts_base(Base())
accepts_base(Derived())

你或许可以看到它的发展方向 – 这个程序实际上是不安全的,并且会在运行时崩溃!具体来说,最后一行被破坏了:我们传递了Derived的实例,而Derived的有趣方法只接受了int.然后它会尝试将它收到的字符串与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 AND的子类型Any是所有其他类型的子类型. Mypy总会选择任何一种不会导致类型检查错误的姿势.

从本质上讲,它是一个逃生舱,一种告诉类型检查器“我知道更好”的方式.每当你给一个变量类型Any时,你实际上完全选择不对该变量进行任何类型检查,无论好坏.

有关详细信息,请参阅typing.Any vs object?.

最后,你能做些什么呢?

好吧,不幸的是,我不确定这是一个简单的方法:你将不得不重新设计你的代码.它基本上是不健全的,并没有任何技巧可以保证让你脱离这一点.

具体如何,这取决于你究竟想做什么.也许你可以用泛型来做一些事情,正如一位用户建议的那样.或者也许您可以将其中一种方法重命名为另一种方法.或者,您可以修改Base.fun,以便它使用与Derived.fun相同的类型,反之亦然;你可以让Derived不再继承Base.这一切都取决于您确切情况的细节.

当然,如果情况真的很棘手,你可以完全放弃在该代码库的那个角落进行类型检查,并使Base.fun(…)接受Any(并接受你可能会开始遇到运行时错误) .

必须考虑这些问题并重新设计您的代码似乎是一个不方便的麻烦 – 但是,我个人认为这是值得庆祝的事情! Mypy成功地阻止了您在代码中意外引入错误,并促使您编写更强大的代码.

标签:python,python-3-x,typing,mypy
来源: https://codeday.me/bug/20190710/1424430.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有