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


174

假设我有多个继承方案:

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__()

但是,在任何一种情况下,如果父类(AB没有遵循相同的约定,则代码将无法正常工作(某些代码可能会丢失或多次调用)。

那么又是什么正确的方法呢?说“保持一致,遵循一个或另一个”很容易,但是如果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

请注意,Binit会被调用两次。如果我做:

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)做出安全选择。


Answers:


78

两种方式都可以正常工作。使用该方法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)继承的类的初始化,否则我无法对我正在编写的类(C)做出安全的选择。

参考的文章显示了如何通过在A和周围添加包装器类来处理这种情况B。标题为“如何合并非合作类”的部分提供了一个可行的示例。

可能希望多重继承更容易,让您轻松组成Car和Airplane类来获得FlyingCar,但现实情况是,单独设计的组件通常需要适配器或包装器,然后才能像我们希望的那样无缝地组装在一起:-)

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


4
不,他们没有。如果B的init不调用super,那么我们执行该super().__init__()方法将不会调用B的init 。如果我打电话A.__init__()B.__init__()直接,然后(如果A和B做电话super)我得到B的初始化被多次调用。
亚当·帕金

3
@AdamParkin(关于您的问题是否已编辑):如果一个父类不是为与super()一起使用而设计的,则通常可以使用添加超级调用的方式来包装它。被引用的文章在标题为“如何合并非合作类”的部分中显示了一个可行的示例。
Raymond Hettinger 2012年

1
当我阅读本文时,以某种方式设法错过了该部分。 正是我想要的。谢谢!
亚当·帕金

1
如果您正在编写python(希望3!)并使用任何形式的继承,尤其是多重继承,则必须阅读rhettinger.wordpress.com/2011/05/26/super-considered-super
肖恩·梅恩

1
致谢是因为我们终于知道为什么我们确定现在可以拥有飞行汽车。
msouth

65

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

有3种不同的方案:

  1. 基类是不相关的独立类。

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

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

    重要:请注意,既不打电话Foo也不Bar打电话super().__init__()!这就是为什么您的代码无法正常工作的原因。由于Diamond继承在python中的工作方式,因此object不应调用基类为的类super().__init__()。如您所知,这样做会破坏多重继承,因为您最终将调用另一个类的__init__而不是object.__init__()免责声明:避免super().__init__()object-subclasses中是我个人的建议,绝不是python社区中达成一致的共识。有些人更喜欢super在每个类中使用,认为如果该类的行为不像您通常可以编写一个适配器您期望的。)

    这也意味着您永远不应编写从其继承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,你的类将支持依赖注入。另一方面,容易出错。例如,如果你改变的顺序FooBar(像class FooBar(Bar, Foo)),你就必须更新super到匹配的电话。没有super您,不必担心这一点,并且代码更具可读性。

  2. 类之一是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. 所有基类均设计用于协作继承。

    专为合作继承而设计的类非常类似于mixin:它们将所有未使用的参数传递给下一类。和以前一样,我们只需要调用即可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__()。如果没有,则不会有合作继承。

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

构造函数是类的公共接口的一部分。如果该类被设计为混合或协作继承,则必须将其记录下来。如果文档中未提及任何内容,则可以安全地假定该类不是为协作多重继承设计的。


2
你的第二点让我震惊。我只在实际的超类的右侧看到过Mixins,并认为它们非常松散和危险,因为您无法检查要混合的类是否具有期望的属性。我从未想过将通用super().__init__(*args, **kwargs)放入mixin并首先编写它。这很有意义。
Minix

10

这两种方法(“新风格”或“旧式”),将工作,如果你有过的源代码控制AB。否则,可能需要使用适配器类。

可访问的源代码:正确使用“新样式”

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__()沿始于的MRO链继续C.__init__进行B.__init__
  • super(B, self).__init__()沿始于的MRO链继续C.__init__进行object.__init__

您可以说这种情况是为多重继承而设计的

可访问的源代码:正确使用“旧样式”

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上的文章super -尤其是“如何合并非合作类”一节)。

第三方家长:A未实施superB确实

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,该MRO在super(Adapter, self).__init__()执行时起作用。

如果反过来呢?

第三方父母:A工具superB才不是

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)做出安全选择。

虽然你可以处理,你没有的情况下,控制的源代码A,并B通过使用适配器类,这是事实,你必须知道在init怎样的父类实现super(如果有的话),以这样做。


4

正如雷蒙德(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")

1
最好的答案恕我直言。发现此方法特别有用,因为它更具前瞻性
Stephen Ellwood


2

如果要从第三方库中繁衍子类类,则不会,没有盲目的方法来调用__init__实际上起作用的基类方法(或任何其他方法),而不管基类的编程方式如何。

super使编写旨在协作实现方法的类成为复杂的多重继承树的一部分成为可能,而类继承者不必知道。但是无法使用它正确地从可能使用或可能不使用的任意类中继承super

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


@RaymondHettinger好吧,您已经在答案中写了一篇文章,并链接到了一篇对此有想法的文章,所以我认为我没有太多补充。:)我认为不可能将任何非超级使用的类通用地适应超级层次结构;您必须提出针对所涉及的特定课程量身定制的解决方案。
2012年
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.