【问题标题】:Inheritance best practice : *args, **kwargs or explicitly specifying parameters继承最佳实践:*args、**kwargs 或显式指定参数
【发布时间】:2013-01-15 14:00:31
【问题描述】:

我经常发现自己覆盖了父类的方法,并且永远无法决定是否应该显式列出给定的参数,或者只是使用一揽子*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 之禅)
  • 更容易掌握
  • 功能参数轻松访问

毯式变体的感知好处

  • 更干燥
  • 父类很容易互换
  • 父方法中默认值的更改无需接触其他代码即可传播

【问题讨论】:

  • 这很大程度上取决于您所子类化的内容...如果您很有可能要向基类的方法添加(或其他人)额外的 kwargs,坚持使用**kwargs 很有意义。如果这不太可能,那么从可读性的角度来看,明确指定 kwargs 肯定要好得多。
  • 好吧,你肯定是在丢弃信息。 显式优于隐式等等。
  • 您有两个选择:1) 显式命名参数和 禁止 派生类中的签名更改(否则使用 super 和多重继承祝你好运),或 2) 使用 @ 987654325@ 并允许更改方法的签名。应该使用哪一个可能取决于具体情况。
  • 使用 *args 和 **kwargs 很难创建正确的文档字符串,vsc 中的智能感知,pycharm 编辑器不接受它们

标签: python inheritance method-signature


【解决方案1】:

里氏替换原则

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

显式签名的好处

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

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

可变长度参数和耦合

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

使用可变长度参数的地方

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

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

你做错了吗?

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

【讨论】:

    【解决方案2】:

    我的选择是:

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

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

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

    【讨论】:

    • 我相信这是最好的答案,因为它解决了问题所暴露的需求。即:保持参数明确与将来向父级添加参数的可能性。尽管@dietbuddha 提供的答案“在学术上”是正确的,但它并没有提供可以在实践中使用的替代方案,当您不确定您的代码未来的命运时。这个答案解决了这个问题。
    • Alex Martelli in this SO thread提供的答案有更多关于这个话题的细节。
    • 你写它的方式暗示父签名也有**kwargs,而问题示例中不是这种情况。
    • 我不知道是不是因为我使用的是 Python 3 但对我来说上面的代码片段不起作用。我需要使用*args, **kwargs,而不仅仅是**kwargs 才能工作。奇怪的是,它也适用于我只使用*args
    【解决方案3】:

    如果您确定 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 中是非常可读的。

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

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

        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)
        

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

        【讨论】:

          【解决方案6】:

          除了其他答案:

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

          如果您正在寻找解耦,请查看composition over inheritance

          【讨论】:

            猜你喜欢
            • 2022-09-22
            • 1970-01-01
            • 2014-11-07
            • 2021-06-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-01-23
            • 2014-03-25
            相关资源
            最近更新 更多