Python的继承是“ is-a”继承样式还是组合样式?


10

考虑到Python允许多重继承,Python中的惯用继承是什么样的?

在具有单一继承的语言(例如Java)中,当您可以说一个对象是另一个对象的“是”并且想要在对象之间共享代码(从父对象到子对象)时,将使用继承。例如,您可以说这Dog是一个Animal

public class Animal {...}
public class Dog extends Animal {...}

但是由于Python支持多重继承,我们可以通过将许多其他对象组合在一起来创建对象。考虑下面的示例:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

这是Python中多重继承的可接受还是良好使用?我们创建一个UserUserService和组成的模型,而不是说一个对象是另一个对象的“是” LoggingService

User通过将数据库或网络操作的所有逻辑放在UserService对象中,并保留用于登录的所有逻辑,可以使其与模型分离LoggingService

我看到这种方法的一些问题是:

  • 这会创造上帝的对象吗?由于User从继承,或由,UserServiceLoggingService是不是真的以下单一职责原则?
  • 为了访问父对象/下一个行对象上的方法(例如,UserService.validate_credentials我们必须使用super。)这使得查看哪个对象将处理此方法变得更加困难,而且不清楚,实例化UserService并执行类似的操作self.user_service.validate_credentials

实现上述代码的Python方式是什么?

Answers:


9

Python的继承是“ is-a”继承样式还是组合样式?

Python支持两种样式。您演示的是具有构成关系的关系,其中用户具有一个来源的日志记录功能和另一个来源的凭证验证。该LoggingServiceUserService基础是混入:他们提供的功能,并且不打算由自己来实例化。

通过在类型中混合混合,您可以拥有一个可以登录但必须添加自己的实例化功能的用户。

这并不意味着不能坚持单一继承。Python也支持。如果您的开发能力因多重继承的复杂性而受到阻碍,那么您可以避免使用它,直到您感到更舒适或在设计中达到了值得权衡的程度。

这会创造上帝的对象吗?

日志记录似乎有些切线-Python确实有自己的带有logger对象的日志记录模块,并且约定是每个模块只有一个日志记录器。

但是将日志记录模块放在一边。也许这违反了单一职责,但是在您的特定情况下,这对于定义用户至关重要。贬低责任可能会引起争议。但是更广泛的原则是Python允许用户做出决定。

超级不清楚吗?

super仅在您需要从同名函数内部以“方法解析顺序”(MRO)委托给父对象时才需要使用“父对象”。最好的做法是使用它而不是硬编码对父方法的调用。但是,如果您不打算对父级进行硬编码,则不需要super

在这里的示例中,您只需要这样做self.validate_credentialsself从您的角度来看还不清楚。他们都遵循MRO。我会在适当的地方简单地使用它们。

如果调用authenticatevalidate_credentials则需要使用super(或对父级进行硬编码)以避免递归错误。

备用代码建议

因此,假设语义还可以(例如日志记录),我要做的是在类中User

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True

1
我不同意。继承总是在其子类中创建类的公共接口的副本。这不是“有”关系。这是子类型化,简单明了,因此不适用于所描述的应用程序。
Jules

@Jules你不同意什么?我说了很多可以证明的事情,并得出了逻辑上可以得出结论的结论。当您说“继承总是在其子类中创建类的公共接口的副本”时,您不正确的。在Python中,没有副本-根据C3算法方法解析顺序(MRO)动态查找方法。
亚伦·霍尔

1
重点不是实现的具体细节,而是类的公共接口的外观。在该示例的情况下,User对象不仅在类的接口中具有在User类中定义的成员,而且在UserService和中定义的成员LoggingService。这不是 “ has-a”关系,因为公共接口是被复制的(尽管不是通过直接复制,而是通过间接查找超类的接口)。
Jules

有-表示组成。Mixins是一种组合形式。User类不是UserService或LoggingService,但它具有该功能。我认为Python的继承与Java的继承比您意识到的要大得多。
亚伦·霍尔

@AaronHall您过于简化了(这往往与您偶然发现的其他答案相矛盾)。从子类型关系的角度来看,用户既是UserService,又是LoggingService。现在,这里的精神是组成功能,以便用户拥有这样的功能。通常,mixin不需要实现多重继承。但是,这是在Python中执行此操作的常用方法。
coredump

-1

除了它允许多个超类的事实外,Python的继承与Java的继承没有本质区别,即子类的成员也是其每个超类型的成员[1]。Python使用鸭子类型的事实也没有区别:您的子类具有其超类的所有成员,因此可以被可以使用这些子类的任何代码使用。通过使用组合有效地实现多重继承的事实是一个危险的事实:将一个类的属性自动复制到另一类是一个问题,无论使用组合还是魔术猜测成员的假定方式都没有关系。工作:拥有他们是错误的。

是的,这违反了单一责任,因为您赋予对象执行逻辑上不属于其设计作用的动作的能力。是的,它创建了“上帝”对象,这实际上是说同一件事的另一种方式。

在Python中设计面向对象的系统时,也适用Java设计手册中讲的相同原则:与继承相比,更喜欢组合。(most [2])其他具有多重继承的系统也是如此。

[1]:您可以将其称为“是”关系,尽管我个人不喜欢该术语,因为它暗示了对现实世界建模的想法,而面向对象的建模与现实世界并不相同。

[2]:我不太了解C ++。C ++支持“私有继承”,它实质上是一种组合,当您要使用继承的类的公共成员时,无需指定字段名称。它根本不影响该类的公共接口。我不喜欢使用它,但是我看不出有任何理由不这样做。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.