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

2024-06-23 19:23:25 发布

您现在位置: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__()!这就是你的代码不能正常工作的原因。由于diamond继承在python中的工作方式,基类为object的类不应该调用super().__init__()。正如您所注意到的,这样做会破坏多重继承,因为您最终会调用另一个类的__init__,而不是object.__init__()免责声明:避免super().__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

    一个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__(),并传递它接收到的任何参数。
    • 子类继承自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的代码,也调用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,但事实是,单独设计的组件通常需要适配器或包装器,然后才能像我们希望的那样无缝地装配在一起:-)

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

如果您控制了AB的源代码,那么无论是“新样式”还是“旧样式”都可以工作。否则,可能需要使用适配器类。

可访问源代码:正确使用“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__B.__init__中启动的MRO链继续。
  • super(B, self).__init__()沿着在C.__init__object.__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__()时起作用。

如果相反呢?

第三方父项: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的情况,但是您必须知道父类的in it是如何实现super(如果有的话)的。

相关问题 更多 >

    热门问题