61

我经常发现自己覆盖了父类的方法,并且永远无法决定是否应该明确列出给定的参数或只使用一揽子*args, **kwargs结构。一个版本比另一个更好吗?有最佳实践吗?我缺少什么(不利)优势?

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 之禅)
  • 更容易掌握
  • 易于访问的函数参数

毯式变体的感知好处

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

6 回答 6

43

里氏替换原则

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

显式签名的好处

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

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

可变长度参数和耦合

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

使用可变长度参数的地方

使用可变长度参数不应该是您的首选。当您有充分的理由时应该使用它,例如:

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

你做错了吗?

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

于 2013-02-08T10:54:06.427 回答
25

我的选择是:

class Child(Parent):

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

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

更新:在这种情况下,如果将新的位置参数添加到父级,则拥有 *args 可能会导致麻烦。我只会保留**kwargs和管理具有默认值的新参数。它将避免错误传播。

于 2013-01-31T13:10:23.067 回答
8

如果您确定 Child 会保留签名,那么明确的方法肯定是更可取的,但是当 Child 更改签名时,我个人更喜欢使用两种方法:

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

这样,签名中的更改在 Child 中非常可读,而原始签名在 Parent 中非常可读。

在我看来,当你有多重继承时,这也是更好的方法,因为super当你没有 args 和 kwargs 时,调用几次是很恶心的。

对于它的价值,这也是很多 Python 库和框架(Django、Tornado、Requests、Markdown 等等)中的首选方式。尽管人们不应该将他的选择基于这些事情,但我只是暗示这种方法非常普遍。

于 2013-02-06T13:40:58.060 回答
4

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

于 2013-02-07T19:16:33.610 回答
4

不是真正的答案,而是更多的旁注:如果您真的非常想确保将父类的默认值传播到子类,您可以执行以下操作:

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)

但是我不得不承认这看起来很丑,我只会在我觉得我真的需要它的时候使用它。

于 2013-02-10T13:11:04.073 回答
0

除了其他答案:

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

如果您正在寻找解耦,请查看组合而不是继承

于 2018-10-26T15:36:01.353 回答