在没有 pylint 抱怨的情况下向重写函数添加额外的参数

Adding additional arguments to overridden functions without pylint complaining

我想在具有一组必需的位置参数的 superclass 中定义一个函数 f,并允许 subclasses 提供它们自己的 f 共享所需的参数,但也有额外的命名参数。

此外,我想以 mypy 能够检查所有类型的方式来执行此操作,而 pylint3 不会抱怨任何事情。

下面的函数 fg 代表了我迄今为止的最佳尝试:

from typing import Any

class Base:
    def f(self, required_arg: int, *args: Any, **kwargs: Any) -> None:
        print(self, required_arg, args, kwargs)
    
    def g(self, required_arg: int, **kwargs: Any) -> None:
        print(self, required_arg, kwargs)

class A(Base):
    def f(self, required_arg: int, x: int = 42, *args: Any, **kwargs: Any) -> None:
        print(self, required_arg, x, args, kwargs)
    
    def g(self, required_arg: int, *, x: int = 42, **kwargs: Any) -> None:
        print(self, required_arg, x, kwargs)

此代码按预期工作(传递 x=20 将其置于 kwargs 中用于基 class,而 x 用于派生 class),但是 运行 pylint3 会产生以下警告:

W: 11, 1: Keyword argument before variable positional arguments list in the definition of f function (keyword-arg-before-vararg)
W: 11, 1: Parameters differ from overridden 'f' method (arguments-differ)
W: 14, 1: Parameters differ from overridden 'g' method (arguments-differ)

当然,有多种方法可以抑制 class 警告,通常是通过文件或行,但我想以这样的方式声明函数没有必要。

如果它有所作为,我实际上并不关心基数 class 是否可以 访问 任何附加参数; kwargs 只是声明的一部分,以允许它接受任意参数。此外,要求 x 作为关键字参数 传递 是可以接受的,只要它以 x 而不是 kwargs['x'] 结束。

您可以在 child class 中使用 parent class 中没有的参数,只要它们是可选的。如果没有 args 和 kwargs,这将是最简单的。

class Animal:
    def speak(self, volume: int) -> None:
        print(f"Animal speaking at volume {volume}")
        
class Parrot(Animal):
    def speak(self, volume: int, words: str ="Pretty Polly") -> None:
        print(f"Parrot says {words} at volume {volume}")
        
a = Animal()
a.speak(4)
p = Parrot()
p.speak(5) # fine, words takes the default as "Pretty Polly"
p.speak(6, "Bye") # Fine, words works from its position
p.speak(7, optional_arg="Yay") # Fine, words can be named

MyPy 对此很满意。 您需要遵循的基本规则是您的 child objects 可以用作 parent objects。在这种情况下,MyPy 很高兴 Parrot 仍然是有效的 Animal,因为它仍然可以只用 volume 调用 p.speak,就像 Animal 要求的那样。

您可以使用仅关键字参数执行相同的操作。

class Parrot(Animal):
    def speak(self, required_arg: int, *, optional_kwarg: str = "Hello again") -> None:
        print(f"A {required_arg} {optional_arg}")

p = P()
p.speak(8) # Fine, as required by base class Animal
p.speak(9, optional_kwarg="Bye again") # fine, as specially allowed of Parrots
p.speak(10, "Oops") # This one won't work, because there isn't a positional argument to put it in.

这通常就是您所需要的。当你知道你有一只 Parrot 时,你可以告诉它该说什么。如果你只知道你有某种动物,因为它可能是一只狗,而狗不会说英语 [需要引证],你应该限制自己使用所有动物都能处理的指令。


可以使用 *args**kwargs 获得您的基础 class。您可以使用仅位置参数标记 /。这样做的代价是您将事情提交给位置参数或关键字参数。语法是这样的:

class Base:
    def f(self, required_pos_arg: int, /, *args, required_kwarg, **kwargs) -> None:
        print(f"Base {required_pos_arg} {required_kwarg} {args} {kwargs}")
        
class A(Base):
    def f(self, required_pos_arg: int, optional_pos_arg: float=1.2, /, *args, required_kwarg, optional_kwarg: str ="Hello", **kwargs) -> None:
        print(f"A {required_pos_arg} {optional_pos_arg} {optional_kwarg} {args} {kwargs}")

b = Base()
b.f(9, 0.2, "Additional_pos_arg", required_kwarg="bar", optional_kwarg="Foo", kwarg_member="Additional_kw_arg")

a = A()
a.f(9, 0.2, "Additional_pos_arg", required_kwarg="bar", optional_kwarg="Foo", kwarg_member="Additional_kw_arg")

通过在位置标记末尾使用 / 标记,并在关键字标记开始之前使用 *(或 *args),您可以消除随之而来的歧义从混淆位置和关键字参数。您可以在 * 之前的列表末尾添加更多位置参数(同样,它们必须是可选的,以便基本调用仍然有效)。你可以在 * 之后添加更多的关键字参数(同样的限制)。你会注意到 child class 也必须接受 argskwargs 因为 child 必须接受 parent 会接受的任何东西。然后 MyPy 又开心了。


但是,正如我之前提到的,这可能不是您想要的。

使用 MyPy 的根本目的是让计算机帮助您确保您只在有意义的地方使用它们。如果您添加 *args**kwargs,您必然会限制它可以完成的工作。它不再能够阻止你让狗背诵麦克白,甚至无法阻止你让鹦鹉用 9.6 的 florgle 说话。

除非在非常特殊的情况下您确实希望基地 class 处理所有事情,否则将自己限制在您真正想做的事情上会更安全。

你当然会发现,如果你写这段代码

def EnglishTest(students: List[Animal]):
    for s in students:
        s.speak(5, words="Double, double toil and trouble")

MyPy 会对你大吼大叫。那不是错误。它正在做它的工作。它告诉你大多数动物不会说话。它会提示您改写

def EnglishTest(students: List[Animal]):
    for s in students:
        if isinstance(s, Parrot):
            s.speak(5, words="Double, double toil and trouble")
        else:
            print(f"{s} failed its English test.")