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

2024-06-23 02:10:09 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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)继承的类的init,否则我无法为我正在编写的类(C)做出安全的选择


Tags: theto方法selfforhereobjectinit
3条回答

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

有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__()免责声明:避免object中的super().__init__()-子类是我个人的建议,绝不是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

    Amixin是一个设计用于多重继承的类。这意味着我们不必手动调用两个父构造函数,因为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__()并传递它接收到的任何参数
    • 子类继承自mixinfirstclass 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__()。如果他们不这样做,就不会有合作继承

一句话:正确的实现取决于您从中继承的类

构造函数为Par类的公共接口的t。如果类被设计为mixin或用于协作继承,则必须对此进行记录。如果文档没有提到这类内容,那么可以安全地假设类不是为协作多重继承而设计的

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

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

当使用super()时,类需要设计为协作多重继承,其中C调用super,调用A的代码,该代码也将调用调用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周围添加包装类来处理这种情况。在题为“如何合并非合作类”的章节中有一个已制定的示例

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

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

如果您可以控制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链继续
  • super(B, self).__init__()沿着在{}到{}中启动的MRO链继续

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

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

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 ^{}-特别是“如何合并非协作类”一节)

第三方家长: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,当执行super(Adapter, self).__init__()时,MRO将发挥作用

如果是另一种情况呢

第三方父级: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首先调用,然后是显式调用。请注意,具有第三方父级的每种情况都需要一个唯一的适配器类

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的源代码的情况,但确实,您必须了解父类的init如何实现super(如果有的话)

相关问题 更多 >