Python 继承:更改子方法的签名

Python 继承:更改子方法的签名,python,oop,inheritance,overriding,Python,Oop,Inheritance,Overriding,我试图找出Python继承原则中的最佳实践是什么,当有一个“坏主意”在子级中更改方法签名时 假设我们有一些基类BaseClient,其中包含已实现的create方法(以及一些抽象方法),这些方法适用于几乎所有的“后代”,除了一个: class BaseClient(object): def __init__(self, connection=None): pass def create(self, entity_id, data=None):

我试图找出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

所以问题是,考虑到最佳python实践,如何以更“pythonic”的方式实现这一点?当然,我们可以在父(子)方法中使用
*args、**kwargs
和其他类似
**kwargs
的方法,但我担心这会降低我的代码的可读性(自文档化)。

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

因此,我假设在
BaseClient
中实现了其他方法,而不是其他子级共享的
create
(否则,没有必要将
ClientC
作为
BaseClient
的子级)。在您的例子中,似乎
ClientC
与其他方法有所不同,它要求对
create
方法进行不同的签名。那么也许是考虑分裂它们的原因了?

例如,您可以让root
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
中使用签名
\u create(self,entity\u id=None,data=None)实现更通用的私有方法来避免代码重复,然后从
EntityBasedClient
DataBasedClient

内部使用适当的参数调用它,我想说的是,只需将参数添加回默认值为None的关键字即可。然后引发一个错误,说明某些输入数据丢失

 class ClientC(BaseClient):
 ....
     def create(self,entity_id=None, data):
         if entity_id:
             raise RedudantInformationError("Value for entity_id does nothing")
         pass
通过这种方式,每当程序员试图像处理其他孩子一样处理孩子C时,他都会收到一条警告,提醒他,但是他可以通过使用try语法一步一步地轻松地做到这一点。

对“我可以更改孩子方法的签名吗?”的回答是肯定的,尽管这是一种非常糟糕的做法

如果您希望在不违反父类的情况下,重写父类的子函数必须具有相同的签名

上面的例子是:

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

违反了原则,并且还会发出警告(“参数与重写的“create”方法不同”)

此外,按照@Sanitiy的建议,为了保持签名的一致性,提出了一个
RedudantInformationError
,这仍然违反了原则,因为如果用父方法代替子方法,则会有不同的行为

另请看:

替换原则:如果子类不能完成基类所能做的事情,那么设计通常是不好的-
x.create(entity_id)
适用于
BaseClient
,因此它应该适用于它的子类。为什么ClientC需要另一个create实现?zch说的是:子类型可以向方法签名添加新的参数,但是它不应该删除它们,因为这违反了。IMO最好在
ClientC
中添加一个类方法,该类方法接受所需的参数并返回该参数的实例。这是可以接受的,因为类的用户在创建实例时已经知道他们想要该子类(因此可能也只知道它拥有的类方法)。如果没有任何其他上下文,我会声明
BaseClient
应该有两种方法:
create\u with_entity
,它与当前的
create
具有相同的签名,以及一个新的
create
,它不接受
实体id
,可能只是
create\u with\u entity
的包装器。我认为这是令人困惑的,因为以前编写的预期BaseClient的函数现在只适用于EntityBasedClient。这使得代码很难理解。ClientC中的
create\u from_data()
方法似乎更健壮。正如Martineau所说,需要从_data()调用create_的代码知道它处理ClientC实例“……以前编写的预期BaseClient的函数现在只能与EntityBasedClient一起工作”。为什么会是这样?正如我所提到的,所有共享方法(也共享签名)都应该在
BaseClient
中实现,因此可以从任何子级访问。或者我遗漏了什么?你的意思是,只要不调用create(),它们仍然可以工作。必须仔细检查每个函数。从OOP的角度来看,在这种情况下,如果函数需要
BaseClient
对象,那么它不应该调用
create
,因为调用哪个实现是不明确的。同样,如果要使用
create\u from\u data
方法,则需要确保对象是
ClientC
的实例。这与确保对象是
DataBasedClient
子对象的实例相同。我想在某些情况下,您的设计可能是正确的,但我们对这些客户机类知之甚少。例如,名为
create()的函数
可能是某种构造函数,这可能会改变它的处理方式。这就是为什么我要求OP告诉我们为什么ClientC需要一个不同的签名。