我有一个做某些事情的父类和两个以正交方式包装方法的子类。当我尝试组合这两个子类时,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。
一个简单的修复方法是使foo
和bar
仅成为关键字参数:
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
的实例传递foo
和bar
的位置参数,因此无论如何都必须将它们作为关键字参数传递,因此不妨将它们声明为关键字参数。
更新
这实际上可能是一个mypy
错误。代码中没有静态类型,因此mypy
应将所有参数的类型读取为Any
,而不报告任何错误。
事实上,如果您使用pyright
,则此代码不会出错:请参阅pyright
游乐场代码
NB:python有几种不同的静态类型检查器:
mypy
:原始(python core-dev)静态类型检查器pyright
:微软的静态类型检查器pyre
:Facebook的静态类型检查器pytype
:谷歌的静态类型检查器
以及执行运行时类型检查:
pydantic
这两个类";不兼容";?
正如@chepner
正确推测的那样,这个问题是由于位置和关键字论点之间的冲突,但他/她的答案并不完全正确。
它们不兼容的原因是,它们在一种方法中打开了关键字冲突的可能性,而在另一种方法则不然。这是一种利斯科夫违规行为。
回答
Python区分参数和自变量。您的函数参数是位置或关键字,这意味着您可以按位置或关键字使用它们。(有时,作者在引用传递给位置或关键字参数的参数时,松散地使用了标准参数一词(如realpython.com的这篇文章),但这不是python在其词汇表中定义的术语。
我将利用mypy游乐场来支持我的主张。
如果从run
函数中删除**kwargs
,实际上不会得到任何错误:
run()
方法中没有**kwargs
这不会失败,因为由于协作多重继承的MRO,C
首先调用Foo
,Foo
调用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()
与我们正在做的混淆。
如果我们考虑添加了**kwargs
的Bar
的情况:
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
作为关键字参数传递,则反之亦然)。
有两种方法可以克服这个错误。如果您希望foo
和bar
仅作为关键字,则第一个由@chepner
显示。
第二种方法是,如果要将foo
和bar
指定为仅定位。以下是mypy游乐场的链接和工作示例:
- 在Python中>3.8,可以使用
/
- 在Python<3.8,您可以在位置arg名称前面加上
__