关于python:继承最佳实践:*args、**kwargs或显式指定参数

Inheritance best practice : *args, **kwargs or explicitly specifying parameters

我经常发现自己在重写父类的方法,并且永远无法决定是应该显式地列出给定的参数,还是只使用一个覆盖的*args, **kwargs构造。一个版本比另一个版本好吗?是否有最佳实践?我缺少哪些优势?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent(object):

    def save(self, commit=True):
        # ...

class Explicit(Parent):

    def save(self, commit=True):
        super(Explicit, self).save(commit=commit)
        # more logic

class Blanket(Parent):

    def save(self, *args, **kwargs):
        super(Blanket, self).save(*args, **kwargs)
        # more logic

显性变异的感知效益

  • 更明确(python的禅)
  • 容易掌握
  • 功能参数易于访问

毛毯变种的感知效益

  • 更干燥
  • 父类很容易互换
  • 父方法中默认值的更改将在不接触其他代码的情况下传播


Liskov替换原则

通常,您不希望方法签名在派生类型中有所不同。如果要交换派生类型的使用,这可能会导致问题。这通常被称为里斯科夫替代原理。

明确签名的好处

同时,我不认为你所有的方法都有*args**kwargs的签名是正确的。显式签名:

  • 帮助通过良好的参数名记录方法
  • 通过指定需要哪些参数以及哪些参数具有默认值,帮助记录该方法
  • 提供隐式验证(缺少必需的参数会引发明显的异常)

变长参数和耦合

不要把变长参数错认为是良好的耦合实践。父类和派生类之间应该有一定的内聚力,否则它们就不会相互关联。相关代码产生反映内聚级别的耦合是正常的。

使用可变长度参数的位置

使用可变长度参数不应该是您的第一个选择。当你有一个很好的理由,比如:

  • 定义函数包装器(即装饰器)。
  • 定义参数多态函数。
  • 当您可以接受的参数确实是完全可变的(例如,通用DB连接函数)时。DB连接函数通常采用多种不同形式的连接字符串,包括单参数形式和多参数形式。对于不同的数据库,也有不同的选项集。

你做错什么了吗?

如果您发现您经常创建采用许多参数的方法或具有不同签名的派生方法,那么您在如何组织代码方面可能会遇到更大的问题。


我的选择是:

1
2
3
4
5
class Child(Parent):

    def save(self, commit=True, **kwargs):
        super(Child, self).save(commit, **kwargs)
        # more logic

它避免从*args**kwargs访问commit参数,并且如果Parent:save的签名发生更改(例如添加新的默认参数),它可以保证安全。

更新:在这种情况下,如果向父级添加新的位置参数,则使用*参数可能会导致问题。我只保留**kwargs,只管理带有默认值的新参数。它可以避免传播错误。


如果您确定孩子会保留签名,那么当然最好使用明确的方法,但是当孩子更改签名时,我个人更喜欢使用这两种方法:

1
2
3
4
5
6
7
8
class Parent(object):
    def do_stuff(self, a, b):
        # some logic

class Child(Parent):
    def do_stuff(self, c, *args, **kwargs):
        super(Child, self).do_stuff(*args, **kwargs)
        # some logic with c

这样,签名中的更改在子签名中是可读的,而原始签名在父签名中是可读的。

在我看来,当你有多重遗产的时候,这也是更好的方法,因为如果你没有args和kwargs的话,几次给super打电话是很恶心的。

就其价值而言,这也是许多Python libs和框架(如Django、Tornado、Requests、Markdown等)中的首选方法。虽然人们不应该把他的选择建立在这样的基础上,但我只是暗示这种方法是相当广泛的。


不是一个真正的答案,而是一个附加说明:如果您真的,真的想要确保父类的默认值被传播到子类,那么您可以执行如下操作:

1
2
3
4
5
6
7
8
9
10
class Parent(object):

    default_save_commit=True
    def save(self, commit=default_save_commit):
        # ...

class Derived(Parent):

    def save(self, commit=Parent.default_save_commit):
        super(Derived, self).save(commit=commit)

不过,我不得不承认,这看起来很难看,只有当我觉得我真的需要它的时候,我才会使用它。


我更喜欢显式参数,因为auto-complete允许您在进行函数调用时查看函数的方法签名。


除其他答案外:

拥有可变参数可能会"分离"父对象和子对象,但会在创建的对象和父对象之间创建耦合,我认为这更糟,因为现在您创建了一个"长距离"耦合(更难发现,更难维护,因为您可以在应用程序中创建多个对象)。

如果您要寻找去耦,请看一看组合而不是继承。