【问题标题】:Inheritance: change signature of child methods继承:更改子方法的签名
【发布时间】:2018-01-13 08:35:03
【问题描述】:

当有一个“坏主意”改变孩子的方法签名时,我试图弄清楚 Python 继承原则中的最佳实践是什么。

假设我们有一些基类 BaseClient 已经实现了 create 方法(和一些抽象方法),它适用于几乎所有的“后代”,除了一个:

 class BaseClient(object):

     def __init__(self, connection=None):
         pass

     def create(self, entity_id, data=None):
         pass

 class ClientA(BaseClient):
     pass

 class ClientB(BaseClient):
     pass

唯一的类 ClientC 需要另一个实现 create 方法并带有一点其他方法签名

 class ClientC(BaseClient):
     ....
     def create(self, data):
         pass

所以问题是如何以更“pythonic”的方式制作它,同时考虑到最佳 python 实践?当然我们可以在父(子)方法中使用*args, **kwargs 和其他类似**kwargs 的方法,但我担心它会降低我的代码的可读性(自我记录)。

【问题讨论】:

  • 替换原则:子类不能做基类可以做的事情通常不是好的设计 - x.create(entity_id) 适用于 BaseClient 所以它应该适用于它的子类。
  • 为什么ClientC需要另一个create实现?
  • zch 所说:子类型可以向方法签名中添加新参数,但不应删除它们,因为这违反了Liskov substitution principle
  • IMO 最好向ClientC 添加一个接受所需参数并返回其实例的类方法。这是可以接受的,因为该类的用户在创建实例时已经知道他们想要该子类(因此大概也只知道它具有的类方法)。
  • 如果没有任何其他上下文,我认为BaseClient 应该有两种方法:create_with_entity,它与您当前的create 具有相同的签名,以及一个新的create,它没有取一个entity_id,可能只是create_with_entity 的包装。

标签: python oop inheritance overriding


【解决方案1】:

我不确定是否有一种 Pythonic 方式可以做到这一点,因为你可以像在问题中那样做。相反,我会说这更多是关于 OOP 而不是 Python 问题。

所以我假设在BaseClient 中实现了其他方法,而不是其他孩子共享的create(否则,没有意义使ClientC 成为BaseClient 的孩子)。在您的情况下,看起来 ClientC 与其他的不同,需要 create 方法的不同签名。那么也许是考虑拆分它们的情况?

例如,您可以让根 BaseClient 实现除 create 之外的所有共享方法,然后再拥有两个 "base" 子级,如下所示:

class EntityBasedClient(BaseClient):
    def create(self, entity_id, data=None):
        pass

class DataBasedClient(BaseClient):
    def create(self, data):
        pass

所以现在你可以继承而不违反任何规则:

class ClientA(EntityBasedClient):
     pass

class ClientB(EntityBasedClient):
     pass

class ClientC(DataBasedClient):
    pass

另外,如果这两个版本的create 实现非常相似,您可以通过在BaseClient 中实现一个更通用的私有方法来避免代码重复,签名为_create(self, entity_id=None, data=None),然后使用适当的参数调用它从EntityBasedClientDataBasedClient 内部。

【讨论】:

  • 我认为这很令人困惑,因为以前编写的期望 BaseClient 的函数现在只能与 EntityBasedClient 一起使用。这使得代码难以理解。 ClientC 中的create_from_data() 方法似乎更健壮。正如 Martineau 所说,需要调用 create_from_data() 的代码知道它处理的是一个 ClientC 实例。
  • "...以前编写的期望 BaseClient 的函数现在只能与 EntityBasedClient 一起使用"。为什么会这样?正如我所提到的,所有共享方法(也共享签名)都应该在BaseClient 中实现,因此任何孩子都可以访问。还是我错过了什么?
  • 您的意思是,只要它们不调用 create(),它们仍然可以工作。必须仔细审查每个功能。
  • 从 OOP 的角度来看,在这种情况下,如果一个函数期待一个 BaseClient 对象,那么它不应该调用 create,因为它会被调用哪个实现是模棱两可的。同样,如果您想使用create_from_data 方法,则需要确保您的对象是ClientC 的实例。这与确保对象是DataBasedClient child 的实例相同。
  • 我想你的设计在某些情况下可能是正确的,但我们对这些客户端类知之甚少。例如,一个名为create() 的函数可能是某种构造函数,这可能会改变应该如何处理。这就是为什么我要求 OP 告诉我们为什么 ClientC 需要不同的签名。
【解决方案2】:

我想说,只需将参数添加回关键字,默认值为无。然后引发一个错误,说明某些输入数据丢失。

 class ClientC(BaseClient):
 ....
     def create(self,entity_id=None, data):
         if entity_id:
             raise RedudantInformationError("Value for entity_id does nothing")
         pass

这样,当程序员尝试像处理其他孩子一样处理孩子 C 时,他会收到一个警告提醒他,但他可以使用 try 语法轻松地绕过。

【讨论】:

    【解决方案3】:

    “我可以更改子方法的签名吗?”的答案?是的,但这是非常糟糕的做法。

    如果您想成为SOLID 并且不违反LSP,则覆盖父类的子函数必须具有相同的签名。

    上面的例子:

    class BaseClient:
        def create(self, entity_id, data=None):
            pass
    
    
    class EntityBasedClient(BaseClient):
        def create(self, entity_id, data=None):
            pass
    
    
    class DataBasedClient(BaseClient):
        def create(self, data):
            pass
    
    

    违反了原则,还会引发 linter 警告(“Parameters different from overridden 'create' method”)

    同样提出RedudantInformationError,正如@Sanitiy 提出的以保持签名的一致性,仍然违反了原则,因为如果使用父方法代替子方法会有不同的行为。

    还请看: Python Method overriding, does signature matter?

    【讨论】:

      猜你喜欢
      • 2011-08-01
      • 2013-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-24
      • 1970-01-01
      • 2021-07-19
      • 1970-01-01
      相关资源
      最近更新 更多