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

Posted

技术标签:

【中文标题】用多重继承调用父类__init__,正确的方法是啥?【英文标题】:Calling parent class __init__ with multiple inheritance, what's the right way?用多重继承调用父类__init__,正确的方法是什么? 【发布时间】:2012-03-23 10:36:34 【问题描述】:

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

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)don't follow the same convention, then the code will not work correctly(有些可能会丢失,或者被多次调用)。

那么正确的方法又是什么?说“保持一致,遵循其中一个”很容易,但如果 AB 来自第 3 方库,那又如何呢?有没有一种方法可以确保所有父类的构造函数都被调用(并且以正确的顺序,并且只调用一次)?

编辑:看看我的意思,如果我这样做:

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 永远不会被调用。因此,除非我知道/控制我继承自的类的 init(AB),否则我无法为我正在编写的类(C)做出安全的选择。

【问题讨论】:

相关:How does Python's super() work with multiple inheritance? 【参考方案1】:

两种方式都可以正常工作。使用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)。

引用的文章展示了如何通过在AB 周围添加一个包装类来处理这种情况。在标题为“如何合并非合作类”的部分中有一个已解决的示例。

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

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

【讨论】:

不,他们没有。如果 B 的 init 没有调用 super,那么如果我们执行 super().__init__() 方法,则不会调用 B 的 init。如果我直接调用A.__init__()B.__init__(),那么(如果A 和B 确实调用super)我会多次调用B 的init。 @AdamParkin(关于您的问题已编辑):如果其中一个父类不是为与 super() 一起使用而设计的,通常可以用某种方式包装它添加 super 调用。引用的文章在标题为“如何合并非合作类”的部分中显示了一个已解决的示例。 当我阅读这篇文章时,不知何故我错过了那个部分。 正是我在寻找什么。谢谢! 如果您正在编写 python(希望是 3 个!)并使用任何类型的继承,尤其是多重继承,那么应该需要阅读 rhettinger.wordpress.com/2011/05/26/super-considered-super。 支持,因为我们终于知道为什么我们现在还没有飞行汽车。【参考方案2】:

如果您要从第三方库中增加子类,那么不,没有盲目的方法来调用基类 __init__ 方法(或任何其他方法),无论基类如何编程,这些方法实际上都有效.

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

本质上,一个类是否被设计为使用super 进行子类化或直接调用基类是属于类的“公共接口”一部分的属性,并且应该这样记录。如果您以库作者期望的方式使用第三方库,并且该库具有合理的文档,它通常会告诉您需要做什么才能子类化特定事物。如果没有,那么您将不得不查看您正在子类化的类的源代码,并查看它们的基类调用约定是什么。如果您以库作者期望的方式组合来自一个或多个第三方库的多个类,则可能无法一致地调用超类方法完全没有;如果 A 类是使用 super 的层次结构的一部分,而 B 类是不使用 super 的层次结构的一部分,则这两个选项都不能保证有效。您必须找出适合每种特定情况的策略。

【讨论】:

@RaymondHettinger 好吧,你已经写了一篇文章并链接到了一篇文章,在你的回答中对此有一些想法,所以我认为我没有太多要补充的。 :) 不过,我认为不可能将任何非超级使用类通用地适应超级层次结构;你必须想出一个针对所涉及的特定类量身定制的解决方案。【参考方案3】:

这篇文章有助于解释协作多重继承:

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/

【讨论】:

【参考方案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")

【讨论】:

最好的答案恕我直言。发现这特别有用,因为它更具前瞻性 简单直接。 2020 年也可与 Python 3 一起使用——它确实是面向未来的 :-)【参考方案5】:

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

有 3 种不同的场景:

    基类是不相关的独立类。

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

    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-subclasses 中避免使用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,您不必担心这一点,并且代码更具可读性。

    其中一个类是 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 的构造函数将永远不会被调用。

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

    为协作继承而设计的类很像 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__()。如果他们不这样做,就不会有合作继承。

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

构造函数是类的公共接口的一部分。如果该类被设计为 mixin 或用于协作继承,则必须记录在案。如果文档没有提及任何此类内容,则可以肯定地假设该类不是为协作多重继承而设计的。

【讨论】:

你的第二点让我大吃一惊。我只在实际超类的右侧看到过 Mixins,并认为它们非常松散和危险,因为您无法检查要混入的类是否具有您期望它具有的属性。我从来没有想过将一个通用的super().__init__(*args, **kwargs) 放入mixin 并先编写它。这很有意义。【参考方案6】:

如果您可以控制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 -&gt; A -&gt; B -&gt; objectsuper(A, self).__init__() 继续沿着在 C.__init__ 中启动的 MRO 链到 B.__init__super(B, self).__init__() 继续沿着在 C.__init__ 中启动的 MRO 链到 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's article on 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,它在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 先调用,再显式调用。请注意,每个具有第三方父级的案例都需要一个唯一的适配器类。

因此,除非我知道/控制我继承自的类的 init(AB),否则我无法为我正在编写的类(C)做出安全的选择。

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

【讨论】:

在可访问的源代码中:正确使用“新样式”,如何打印 -> B & B" & " @ShambhavAgrawal 正如您提到的部分所述,此示例的 MRO 是 C -&gt; A -&gt; B -&gt; object。每当调用super() 时,执行都会向上传递到 MRO 链。所以super(A, self).__init__() 传递给B.__init__()(打印“-> B”和“super(B, self).__init__() 传递给object.__init__()(不打印任何内容)。【参考方案7】:

我添加了一个小型实用程序库supers,它使这种场景更易于处理。它的工作原理如下:

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")
        supers(self).__init__()
        print("Leaving C")

创建 C 时的输出:

Entering C
Entering A
Leaving A
Entering B
Leaving B
Leaving C

【讨论】:

【参考方案8】:

1.首先,假设你有 MRO 链

2.从最底层的子类init方法开始,任何使用super()方法的类都会跳转到对应的链位置,任何不使用super()方法的类都会跳出对应的链位置。

如有错误请指正!

【讨论】:

【参考方案9】:

它遵循MRO规则,调用了一个init

【讨论】:

【参考方案10】:

这是我在 python 3 中使用 super() 实现多重继承的方法

class A:
  def __init__(self,a,b,**kwargs):
      print("Class A initiallised")
      self.a=a
      self.b=b
      super().__init__(**kwargs)
      print("Class A initiallisation done")

  def __str__(self):
      return f"self.a and self.b"

  def display_a(self):
      return f"self.a and self.b"

class C:
   def __init__(self,c,d,**kwargs):
      print("Class C initiallised")
      self.c=c
      self.d=d
      super().__init__(**kwargs)
      print("class c initiallisation done")

   def __str__(self):
      return f"self.c and self.d"

   def display_c(self):
       return f"self.c and self.d"


class D(A,C):
   def __init__(self,e,**kwargs):
       print("Class D initiallised")
       super().__init__(**kwargs)
       self.e=e
       print("Class D initiallisation done")

   def __str__(self):
      return f"self.e is e,self.b is b,self.a is a,self.d is d,self.c is c"

if __name__=="__main__":
   d=D(a=12,b=13,c=14,d=15,e=16)
   print(d)
   d.display_c()
   d.display_a()

【讨论】:

【参考方案11】:

这是我在python继承中实现super方法并实现所需解决方案的方法:

class A:
    def __init__(self):
        print("from A")

class B:
    def __init__(self):
        print("from B")
class C(A,B):
    def __init__(self):
          A.__init__(self)
          B.__init__(self)
          print ("from C")
c = C()

【讨论】:

以上是关于用多重继承调用父类__init__,正确的方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

多重继承

面向对象之继承——python篇

super,多继承

python 多重继承,继承的几个父类都需要传递参数,怎么在子类计算出父类传递的参数总和呢?

python--多重继承

python中多重继承与获取对象