关于python:用多重继承调用父类 __init__ ,正确的方法是什么?

Calling parent class __init__ with multiple inheritance, what's the right way?

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

1
2
3
4
5
6
7
8
9
10
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__有两种典型的方法:

  • (老式)ParentClass.__init__(self)
  • (新款)super(DerivedClass, self).__init__()
  • 但是,在这两种情况下,如果父类(AB不遵循相同的约定,那么代码将无法正常工作(有些可能会丢失,或者多次调用)。

    那么,正确的方法又是什么呢?很容易说"只是保持一致,跟着一个或另一个",但是如果AB来自第三方库,那么呢?是否有一种方法可以确保调用所有父类构造函数(并且顺序正确,而且只调用一次)?

    编辑:要了解我的意思,如果我这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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")

    然后我得到:

    1
    2
    3
    4
    5
    6
    7
    8
    Entering C
    Entering A
    Entering B
    Leaving B
    Leaving A
    Entering B
    Leaving B
    Leaving C

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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")

    然后我得到:

    1
    2
    3
    4
    Entering C
    Entering A
    Leaving A
    Leaving C

    注意,不会调用B的init。因此,如果我不知道/控制我继承的类(AB的init,我就不能为我正在编写的类(C作出一个安全的选择。


    两种方法都可以。使用super()的方法使子类具有更大的灵活性。

    在直接呼叫方式下,C.__init__可以同时呼叫A.__init__B.__init__

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

    [稍后编辑的回答问题]

    So it seems that unless I know/control the init's of the classes I
    inherit from (A and B) I cannot make a safe choice for the class I'm
    writing (C).

    参考文章展示了如何通过在AB周围添加一个包装类来处理这种情况。在题为"如何合并一个非合作类"的章节中,有一个已经制定好的例子。

    有人可能希望多继承更容易,让您轻松地组合汽车和飞机类以获得一辆FlyingCar,但现实是,单独设计的组件在无缝装配之前通常需要适配器或包装器,正如我们希望的那样:—)

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


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

    有三种不同的情况:

  • 基类是不相关的独立类。

    如果您的基类是单独的实体,它们能够独立运行,并且彼此不认识,那么它们不是为多重继承而设计的。例子:

    1
    2
    3
    4
    5
    6
    7
    class Foo:
        def __init__(self):
            self.foo = 'foo'

    class Bar:
        def __init__(self, bar):
            self.bar = bar

    重要提示:注意,FooBar都没有调用super().__init__()!这就是您的代码不能正常工作的原因。由于Diamond继承在Python中的工作方式,其基类为object的类不应调用super().__init__()。正如您所注意到的,这样做会破坏多重继承,因为您最终会调用另一个类的__init__,而不是object.__init__()。(免责声明:避免object子类中的super().__init__()是我个人的建议,决不是在python社区达成一致意见。有些人更喜欢在每个类中使用super,认为如果类的行为不符合您的预期,则可以始终编写适配器。)

    这也意味着您不应该编写继承自object且没有__init__方法的类。完全不定义__init__方法与调用super().__init__()具有相同的效果。如果类直接从object继承,请确保添加一个空的构造函数,如下所示:

    1
    2
    3
    class Base(object):
        def __init__(self):
            pass

    无论如何,在这种情况下,您必须手动调用每个父构造函数。有两种方法可以做到这一点:

    • super

      1
      2
      3
      4
      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • super一起

      1
      2
      3
      4
      5
      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,您的类将支持依赖注入。另一方面,犯错更容易。例如,如果更改FooBar的顺序(如class FooBar(Bar, Foo)),则必须更新super调用以匹配。如果没有super,就不必担心这个问题,而且代码的可读性要高得多。

  • 其中一个类是mixin。

    mixin是设计用于多继承的类。这意味着我们不必手动调用两个父构造函数,因为mixin将自动为我们调用第二个构造函数。因为我们这次只需要调用一个构造函数,所以我们可以使用super这样做,以避免硬编码父类的名称。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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的构造函数。
  • 所有的基类都是为合作继承而设计的。

    为合作继承设计的类与mixin非常相似:它们将所有未使用的参数传递给下一个类。和以前一样,我们只需要调用super().__init__(),所有父构造函数都将被链调用。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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__()。如果他们不这样做,就不会有合作继承。

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

    构造函数是类的公共接口的一部分。如果该类被设计为混合继承或合作继承,那么必须对其进行记录。如果文档没有提到这类内容,那么可以安全地假定该类不是为合作的多重继承而设计的。


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

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

    它提到了显示方法分辨率顺序的有用方法mro()。在第二个示例中,当您在A中调用super时,super调用将在mro中继续。顺序中的下一个类是B,这就是为什么第一次调用B的init。

    下面是来自官方python站点的技术性文章:

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


    如果您从第三方库中复制子类,那么不,没有盲目的方法来调用实际工作的基类__init__方法(或任何其他方法),而不管基类是如何编程的。

    super使得编写设计用于作为复杂多继承树的一部分(类作者不需要知道这些树)协同实现方法的类成为可能。但是没有办法使用它来正确继承可能使用或不使用super的任意类。

    从本质上讲,一个类是被设计成使用super进行子类化,还是直接调用基类,这是类"公共接口"的一部分属性,应该这样记录。如果您以图书馆作者预期的方式使用第三方图书馆,并且图书馆有合理的文档,那么它通常会告诉您需要做什么来对特定的事情进行子类划分。如果没有,那么您将必须查看子类的类的源代码,并查看它们的基类调用约定是什么。如果您以库作者没有预料到的方式组合来自一个或多个第三方库的多个类,那么可能根本不可能一致地调用超级类方法;如果使用super的层次结构中包含类A,而B是不使用super的层次结构中的一部分,那么这两个选项都不能保证D工作。你必须找出一个适合每个特定案例的策略。


    正如Raymond在回答中所说,直接调用A.__init__B.__init__可以很好地工作,而且您的代码是可读的。

    但是,它不使用C和这些类之间的继承链接。利用这个链接可以使您更加一致,并使最终的重构更容易,也不容易出错。如何做到这一点的示例:

    1
    2
    3
    4
    5
    6
    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")


    如果您控制了AB的源代码,那么任何一种方法("新样式"或"旧样式")都可以工作。否则,可能需要使用适配器类。

    源代码可访问:正确使用"新样式"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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")
    1
    2
    3
    4
    5
    6
    7
    >>> C()
    -> C
    -> A
    -> B
    <- B
    <- A
    <- C

    这里,方法解析顺序(MRO)规定了以下内容:

    • C(A, B)首先指示A,然后指示B。MRO是C -> A -> B -> object
    • super(A, self).__init__()继续沿着C.__init__B.__init__启动的MRO链。
    • super(B, self).__init__()继续沿着C.__init__object.__init__启动的MRO链。

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

    源代码可访问:正确使用"旧样式"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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")
    1
    2
    3
    4
    5
    6
    7
    >>> C()
    -> C
    -> A
    <- A
    -> B
    <- B
    <- C

    这里,MRO并不重要,因为A.__init__B.__init__是明确调用的。class C(B, A):也可以。

    尽管这种情况并不像前一种样式那样"设计"用于新样式中的多个继承,但仍然可以进行多个继承。

    现在,如果AB来自第三方库,即您无法控制AB的源代码,该怎么办?简而言之:您必须设计一个实现必要的super调用的适配器类,然后使用一个空类来定义mro(参见Raymond Hettinger关于super的文章,特别是"如何合并一个非合作类"一节)。

    第三方家长:A不执行superB执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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
    1
    2
    3
    4
    5
    6
    7
    >>> C()
    -> C
    -> A
    <- A
    -> B
    <- B
    <- C

    Adapter实现super,以便C定义执行super(Adapter, self).__init__()时生效的MRO。

    如果是另一条路呢?

    第三方父母:A执行superB不执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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
    1
    2
    3
    4
    5
    6
    7
    >>> C()
    -> C
    -> A
    <- A
    -> B
    <- B
    <- C

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

    So it seems that unless I know/control the init's of the classes I inherit from (A and B) I cannot make a safe choice for the class I'm writing (C).

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