首页 文章

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

提问于
浏览
106

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

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

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

那么's the correct way again? It'很容易说"just be consistent, follow one or the other",但如果 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 's init never gets called. So it seems that unless I know/control the init'( AB )我无法为我正在编写的类( C )做出安全选择 .

5 回答

  • 22

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

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

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

    [后面编辑的回复问题]

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

    引用的文章展示了如何通过在 AB 周围添加包装类来处理这种情况 . Headers 为"How to Incorporate a Non-cooperative Class"的部分有一个经过深思熟虑的例子 .

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

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

  • 2

    你的问题的答案取决于一个非常重要的方面: Are your base classes designed for multiple inheritance?

    有3种不同的场景:

    基类是不相关的独立类 .

    如果您的基类是能够独立运行的独立实体,并且它们不是为多重继承而设计的 . 例:

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

    Important: 请注意, FooBar 都没有调用 super().__init__() !这就是您的代码无法正常工作的原因 . 由于钻石继承在python中的工作方式, classes whose base class is object should not call super().init() . 当你've noticed, doing so would break multiple inheritance because you end up calling another class' s __init__ 而不是 object.__init__() . ( Disclaimer: 避免在 object -subclasses中使用 super().__init__() 是我个人的建议,并不是在python社区中达成共识 . 有些人更喜欢在每个 class 中使用 super ,认为如果 class 没有,你总是可以写一个adapter表现得像你期望的那样 . )

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

    class Base(object):
        def __init__(self):
            pass
    

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

    • Without super
    class FooBar(Foo, Bar):
        def __init__(self, bar='bar'):
            Foo.__init__(self)  # explicit calls without super
            Bar.__init__(self, bar)
    
    • With 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 ,您的 class 将支持dependency injection . 另一方面,犯错更容易 . 例如,如果更改 FooBar (如 class FooBar(Bar, Foo) )的顺序,则必须更新 super 调用才能匹配 . 如果没有 super ,您不必担心这一点,并且代码更具可读性 .

    其中一个类是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的构造函数 .

    所有基类都是为协作继承而设计的 .

    为协作继承而设计的类很像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 继承,代码仍然可以正常工作 . 但是's only true because all arguments are passed as keyword arguments. Using positional arguments would make it easy to get the order of the arguments wrong, so it'习惯于合作类只接受关键字参数 .

    这也是我之前提到的规则的一个例外: CoopFooCoopBar 都从 object 继承,但它们仍然调用 super().__init__() . 如果他们没有,就没有合作继承 .

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

    构造函数是类的一部分's public interface. If the class is designed as a mixin or for cooperative inheritance, that must be documented. If the docs don'提到任何类型的东西,可以安全地假设该类不是为协作多重继承而设计的 .

  • 1

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

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

    它提到了有用的方法 mro() ,它显示了方法解析顺序 . 在您的第二个示例中,您在 A 中调用 supersuper 呼叫在MRO中继续 . 顺序中的下一个类是 B ,这就是第一次调用 B 的init的原因 .

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

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

  • 3

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

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

    从本质上讲,一个类是使用 super 设计为子类还是直接调用基类是一个属性,它是类库的一部分,使用第三方库,就像库作者所期望的那样,并且库有合理的文档,它通常会告诉你你需要做什么来分类特定的东西 . 如果没有,那么你'll have to look at the source code for the classes you'重新进行子类化,看看他们的基类调用约定是什么 . 如果您以一种图书馆作者不期望的方式组合来自一个或多个第三方库的多个类,那么根本不可能一致地调用超类方法;如果A类是使用 super 的层次结构的一部分,而B类是层次结构的一部分,那么它必须找出恰好适用于每个特定情况的策略 .

  • 53

    正如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")
    

相关问题