Mypy方法定义与基类中的定义不兼容



我有一个做某些事情的父类和两个以正交方式包装方法的子类。当我尝试组合这两个子类时,mypy会抱怨以下错误。

基类"Foo"中"run"的定义与基本类"条形"中的定义

这两个类是如何"不兼容"的?我如何修改代码以安抚mypy?我可以不做吗

class Parent:
def run(self, a, b):
pass

class Foo(Parent):
def run(self, a, b, foo=None, **kwargs):
print('foo', foo)
super().run(a, b, **kwargs)

class Bar(Parent):
def run(self, a, b, bar=None, **kwargs):
print('bar', bar)
super().run(a, b, **kwargs)

class C(Foo, Bar):
pass

Mypy之所以抱怨,只是因为您试图从Foo和Bar继承C,这两个方法运行相同,但参数不同。为了让Mypy高兴,您必须使Foo和Bar类中的运行函数相同(匹配相同的函数签名),或者不从这两个类继承类C。

一个简单的修复方法是使foobar仅成为关键字参数:

class Parent:
def run(self, a, b):
pass

class Foo(Parent):
def run(self, a, b,*,foo=None, **kwargs):
print('foo', foo)
super().run(a, b,  **kwargs)

class Bar(Parent):
def run(self, a, b,*,bar=None, **kwargs):
print('bar', bar)
super().run(a, b, **kwargs)

class C(Foo, Bar):
pass

使用位置参数的问题是,如果要进行像C().run(1,2,3)这样的调用,mypy无法判断每个调用应该对3做什么。把C想象成Foo,它应该是foo的值。但把C看作Bar,它应该是bar的值。通常,在overriden方法的签名中添加位置参数是个坏主意,即使在运行时3将绑定到最先调用的方法的第一个可用参数。

此外,由于无法通过C的实例传递foobar的位置参数,因此无论如何都必须将它们作为关键字参数传递,因此不妨将它们声明为关键字参数。

更新

这实际上可能是一个mypy错误。代码中没有静态类型,因此mypy应将所有参数的类型读取为Any,而不报告任何错误。

事实上,如果您使用pyright,则此代码不会出错:请参阅pyright游乐场代码

NBpython有几种不同的静态类型检查器

  • mypy:原始(python core-dev)静态类型检查器
  • pyright:微软的静态类型检查器
  • pyre:Facebook的静态类型检查器
  • pytype:谷歌的静态类型检查器

以及执行运行时类型检查:

  • pydantic

这两个类";不兼容";?

正如@chepner正确推测的那样,这个问题是由于位置和关键字论点之间的冲突,但他/她的答案并不完全正确。

它们不兼容的原因是,它们在一种方法中打开了关键字冲突的可能性,而在另一种方法则不然。这是一种利斯科夫违规行为。

回答

Python区分参数和自变量。您的函数参数位置或关键字,这意味着您可以按位置或关键字使用它们。(有时,作者在引用传递给位置或关键字参数的参数时,松散地使用了标准参数一词(如realpython.com的这篇文章),但这不是python在其词汇表中定义的术语。

我将利用mypy游乐场来支持我的主张。

如果从run函数中删除**kwargs,实际上不会得到任何错误:

  • run()方法中没有**kwargs

这不会失败,因为由于协作多重继承的MRO,C首先调用FooFoo调用Bar是因为您使用了super。你甚至不能用bar调用c.run(),因为它没有:

>>> c.run(1, 2, bar='baz')
Traceback (most recent call last):
File "demo.py", line 27, in <module>
c.run(1, 2, bar='baz')
TypeError: run() got an unexpected keyword argument 'bar'

这都是确定性的,所以这里没有歧义的可能性。

添加**kwargs时会出现问题。正因为如此,你可以让一个函数工作,但不能让另一个工作。

在任何情况下,无论何时使用**kwargs,都有意外传递与函数中现有参数同名的关键字参数的风险。例如,如果我们带回**kwargs

...
class Foo(Parent):
def run(self, a, b, foo, **kwargs):
print('foo', foo)
super().run(a, b, **kwargs)
...

然后像这样调用c,我们得到一个错误:

>>> c.run(1, 2, 3, foo='baz')
Traceback (most recent call last):
File "demo.py", line 27, in <module>
c.run(1, 2, 3, foo='baz')
TypeError: run() got multiple values for argument 'foo'

因为您的参数是标准的,foo可以是位置或关键字,所以我们将这里的run()与我们正在做的混淆。

如果我们考虑添加了**kwargsBar的情况:

class Bar(Parent):
def run(self, a, b, bar, **kwargs):
print('bar', bar)
super().run(a, b, **kwargs)

我们可以用关键字参数foo调用Bar.run(),因为foo不在Bar.run():的参数列表中

>>> b = Bar()
>>> b.run(1, 2, 3, foo='baz')
bar 3

因此,存在不兼容性,因为您可以用一种方式调用Bar,但不能用相同的参数调用Foo(如果我们将bar作为关键字参数传递,则反之亦然)。

有两种方法可以克服这个错误。如果您希望foobar仅作为关键字,则第一个由@chepner显示。

第二种方法是,如果要将foobar指定为仅定位。以下是mypy游乐场的链接和工作示例:

  • 在Python中>3.8,可以使用/
  • 在Python<3.8,您可以在位置arg名称前面加上__

最新更新