使用多重继承调用父类__init__,这是正确的方法吗?

问题描述 投票:111回答:6

假设我有一个多继承场景:

class A(object):
    # code for A here

class B(object):
    # code for B here

class C(A, B):
    def __init__(self):
        # What's the right code to write here to ensure 
        # A.__init__ and B.__init__ get called?

C__init__有两种典型的方法:

  1. (旧式)ParentClass.__init__(self)
  2. (较新式)super(DerivedClass, self).__init__()

但是,在任何一种情况下,如果父类(ABdon't follow the same convention, then the code will not work correctly(有些可能会错过,或被多次调用)。

那么又是什么样的正确方法呢?很容易说“只是保持一致,遵循一个或另一个”,但如果AB来自第三方图书馆,那么呢?有没有一种方法可以确保所有父类构造函数被调用(并且以正确的顺序,只有一次)?

编辑:看看我的意思,如果我这样做:

class A(object):
    def __init__(self):
        print("Entering A")
        super(A, self).__init__()
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

class C(A, B):
    def __init__(self):
        print("Entering C")
        A.__init__(self)
        B.__init__(self)
        print("Leaving C")

然后我得到:

Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C

请注意,B的init被调用两次。如果我做:

class A(object):
    def __init__(self):
        print("Entering A")
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

class C(A, B):
    def __init__(self):
        print("Entering C")
        super(C, self).__init__()
        print("Leaving C")

然后我得到:

Entering C
Entering A
Leaving A
Leaving C

请注意,B的init永远不会被调用。所以似乎除非我知道/控制我继承的类的初始化(AB),否则我无法为我写的类(C)做出安全的选择。

python oop inheritance multiple-inheritance super
6个回答
58
投票

两种方式都很好。使用super()的方法为子类带来了更大的灵活性。

在直接调用方法中,C.__init__可以调用A.__init__B.__init__

当使用super()时,类需要设计用于协同多重继承,其中C调用super,它调用A的代码,该代码也将调用调用super代码的B。有关http://rhettinger.wordpress.com/2011/05/26/super-considered-super可以做些什么的更多详细信息,请参阅super

[后面编辑的回复问题]

因此,除非我知道/控制从(A和B)继承的类的init,否则我无法为我正在编写的类(C)做出安全选择。

参考文章展示了如何通过在AB周围添加包装类来处理这种情况。标题为“如何合并非合作类”的部分有一个经过深思熟虑的例子。

有人可能希望多继承更容易,让你毫不费力地组成Car和Airplane类来获得FlyingCar,但实际情况是,单独设计的组件通常需要适配器或包装器才能像我们希望的那样无缝地组合在一起:-)

另一个想法是:如果您对使用多重继承编写功能感到不满意,可以使用组合来完全控制在哪些情况下调用哪些方法。


29
投票

您的问题的答案取决于一个非常重要的方面:您的基类是否设计用于多重继承?

有3种不同的场景:

  1. 基类是不相关的独立类。 如果您的基类是能够独立运行且彼此不了解的独立实体,则它们不是为多重继承而设计的。例: class Foo: def __init__(self): self.foo = 'foo' class Bar: def __init__(self, bar): self.bar = bar 重要提示:请注意,FooBar都没有调用super().__init__()!这就是您的代码无法正常工作的原因。由于钻石继承在python中的工作方式,基类为object的类不应该调用super().__init__()。正如你所注意到的那样,这样做会破坏多重继承,因为你最终会调用另一个类的__init__而不是object.__init__()。 (免责声明:在super().__init__()子类中避免使用object是我个人的建议,并不是在python社区中达成一致的共识。有些人更喜欢在每个班级使用super,认为如果班级没有,你总是可以写一个adapter'表现得像你期望的那样。) 这也意味着你永远不应该编写一个继承自object并且没有__init__方法的类。根本没有定义__init__方法与调用super().__init__()具有相同的效果。如果你的类直接从object继承,请确保添加一个空的构造函数,如下所示: class Base(object): def __init__(self): pass 无论如何,在这种情况下,您必须手动调用每个父构造函数。有两种方法可以做到这一点: 没有super class FooBar(Foo, Bar): def __init__(self, bar='bar'): Foo.__init__(self) # explicit calls without super Bar.__init__(self, bar) 随着super class FooBar(Foo, Bar): def __init__(self, bar='bar'): super().__init__() # this calls all constructors up to Foo super(Foo, self).__init__(bar) # this calls all constructors after Foo up # to Bar 这两种方法中的每一种都有其自身的优点和缺点。如果你使用super,你的班级将支持dependency injection。另一方面,犯错更容易。例如,如果您更改FooBar(如class FooBar(Bar, Foo))的顺序,则必须更新super调用以匹配。如果没有super,您不必担心这一点,并且代码更具可读性。
  2. 其中一个类是mixin。 mixin是一个专门用于多重继承的类。这意味着我们不必手动调用两个父构造函数,因为mixin将自动为我们调用第二个构造函数。由于这次我们只需要调用一个构造函数,因此我们可以使用super来避免必须对父类的名称进行硬编码。 例: class FooMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # forwards all unused arguments self.foo = 'foo' class Bar: def __init__(self, bar): self.bar = bar class FooBar(FooMixin, Bar): def __init__(self, bar='bar'): super().__init__(bar) # a single call is enough to invoke # all parent constructors # NOTE: `FooMixin.__init__(self, bar)` would also work, but isn't # recommended because we don't want to hard-code the parent class. 这里的重要细节是: mixin调用super().__init__()并传递它收到的任何参数。 子类首先从mixin继承:class FooBar(FooMixin, Bar)。如果基类的顺序错误,则永远不会调用mixin的构造函数。
  3. 所有基类都是为协作继承而设计的。 为协作继承而设计的类很像mixins:它们将所有未使用的参数传递给下一个类。像以前一样,我们只需调用super().__init__(),所有父构造函数都将被链调用。 例: class CoopFoo: def __init__(self, **kwargs): super().__init__(**kwargs) # forwards all unused arguments self.foo = 'foo' class CoopBar: def __init__(self, bar, **kwargs): super().__init__(**kwargs) # forwards all unused arguments self.bar = bar class CoopFooBar(CoopFoo, CoopBar): def __init__(self, bar='bar'): super().__init__(bar=bar) # pass all arguments on as keyword # arguments to avoid problems with # positional arguments and the order # of the parent classes 在这种情况下,父类的顺序无关紧要。我们可能首先从CoopBar继承,代码仍然可以正常工作。但这只是因为所有参数都作为关键字参数传递。使用位置参数可以很容易地使参数的顺序错误,因此合作类通常只接受关键字参数。 这也是我之前提到的规则的一个例外:CoopFooCoopBar都继承自object,但他们仍称super().__init__()。如果他们没有,就没有合作继承。

底线:正确的实现取决于您继承的类。

构造函数是类的公共接口的一部分。如果将类设计为mixin或用于协作继承,则必须记录该类。如果文档没有提到任何类型的东西,可以安全地假设该类不是为协作多重继承而设计的。


3
投票

本文有助于解释合作多重继承:

http://www.artima.com/weblogs/viewpost.jsp?thread=281127

它提到了有用的方法mro(),它显示了方法解析顺序。在你的第二个例子中,你在super中调用Asuper调用继续在MRO中。顺序中的下一个类是B,这就是为什么第一次调用B的init。

这是来自官方python网站的更多技术文章:

http://www.python.org/download/releases/2.3/mro/


2
投票

如果你是来自第三方库的多级子类,那么不,没有盲目的方法来调用实际工作的基类__init__方法(或任何其他方法),无论基类如何编程。

super可以编写用于协作实现方法的类,作为复杂多重继承树的一部分,这些树不需要为类作者所知。但是没有办法使用它来正确地继承任何可能使用或不使用super的类。

从本质上讲,一个类是设计为使用super进行子类化还是直接调用基类是属于类“公共接口”的属性,并且应该如此记录。如果您以库作者期望的方式使用第三方库并且库具有合理的文档,则通常会告诉您要对特定事物进行子类化所需的操作。如果没有,那么你将不得不查看你正在进行子类化的类的源代码,看看他们的基类调用约定是什么。如果您以一种图书馆作者不期望的方式组合来自一个或多个第三方库的多个类,那么根本不可能一致地调用超类方法;如果A类是使用super的层次结构的一部分而B类是不使用super的层次结构的一部分,那么这两个选项都不能保证工作。您必须找出适合每个特定案例的策略。


2
投票

如果您可以控制AB的源代码,那么任何一种方法(“新风格”或“旧风格”)都可以使用。否则,可能需要使用适配器类。

Source code accessible: Correct use of "new style"

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        # Use super here, instead of explicit calls to __init__
        super(C, self).__init__()
        print("<- C")
>>> C()
-> C
-> A
-> B
<- B
<- A
<- C

这里,方法解析顺序(MRO)规定如下:

  • C(A, B)首先决定A,然后B。 MRO是C -> A -> B -> object
  • super(A, self).__init__()沿着在C.__init__发起的MRO链继续到B.__init__
  • super(B, self).__init__()沿着在C.__init__发起的MRO链继续到object.__init__

你可以说这个案例是为多重继承而设计的。

Source code accessible: Correct use of "old style"

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        # Don't use super here.
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        B.__init__(self)
        print("<- C")
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

在这里,MRO无关紧要,因为A.__init__B.__init__被明确调用。 class C(B, A):也会起作用。

虽然这种情况不像前一个那样“设计”为新样式的多重继承,但仍然可以进行多重继承。


现在,如果AB来自第三方库,即你无法控制AB的源代码呢?简短回答:您必须设计一个实现必要的super调用的适配器类,然后使用空类来定义MRO(请参阅Raymond Hettinger's article on super - 尤其是“如何合并非合作类”一节)。

Third-party parents: A does not implement super; B does

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        super(Adapter, self).__init__()
        print("<- C")

class C(Adapter, B):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

Adapter实现super,以便C可以定义MRO,它在执行super(Adapter, self).__init__()时发挥作用。

如果是相反的话怎么办?

Third-party parents: A implements super; B does not

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        super(Adapter, self).__init__()
        B.__init__(self)
        print("<- C")

class C(Adapter, A):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

这里的模式相同,除了执行顺序在Adapter.__init__中切换; super首先调用,然后显式调用。请注意,每个具有第三方父项的案例都需要一个唯一的适配器类。

所以似乎除非我知道/控制我继承的类的初始化(AB),否则我无法为我写的类(C)做出安全的选择。

虽然您可以通过使用适配器类来处理不控制AB的源代码的情况,但是您必须知道父类的init如何实现super(如果有的话)才能执行所以。


1
投票

正如Raymond在他的回答中所说,直接调用A.__init__B.__init__工作正常,你的代码是可读的。

但是,它不使用C和那些类之间的继承链接。利用该链接可以提供更多的包含性,并使最终的重构更容易,更不容易出错。如何做到这一点的一个例子:

class C(A, B):
    def __init__(self):
        print("entering c")
        for base_class in C.__bases__:  # (A, B)
             base_class.__init__(self)
        print("leaving c")
© www.soinside.com 2019 - 2024. All rights reserved.