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

2024-03-24

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

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's __init__:

  1. (老式)ParentClass.__init__(self)
  2. (较新风格)super(DerivedClass, self).__init__()

但是,无论哪种情况,如果父类 (A and B) 不遵循相同的约定,那么代码将无法正常工作 http://fuhm.org/super-harmful/(有些可能会错过,或被多次呼叫)。

那么正确的方法又是什么呢?说“保持一致,遵循其中一个或另一个”很容易,但如果A or B来自第三方库,然后呢?是否有一种方法可以确保所有父类构造函数都被调用(并且以正确的顺序,并且仅调用一次)?

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

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 永远不会被调用。所以看来除非我知道/控制我继承的类的初始化(A and B)我无法为我正在写的课程做出安全的选择(C).


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

有 3 种不同的场景:

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

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

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

    重要的:请注意,两者都没有Foo nor Bar calls super().__init__()!这就是您的代码无法正常工作的原因。由于 python 中菱形继承的工作方式,基类是的类object不应该打电话super().__init__()。正如您所注意到的,这样做会破坏多重继承,因为您最终会调用另一个类的__init__而不是object.__init__(). (免责声明:避免super().__init__() in object-subclasses 是我个人的建议,绝不是 python 社区达成的共识。有些人更喜欢使用super在每堂课上,争论你总是可以写一个adapter https://en.wikipedia.org/wiki/Adapter_pattern如果类的行为不符合您的预期。)

    这也意味着您永远不应该编写继承自的类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,你的班级将支持依赖注入 https://stackoverflow.com/a/33469090/1222951。另一方面,也更容易犯错误。例如,如果您更改顺序Foo and Bar (like class FooBar(Bar, Foo)),你必须更新super调用进行匹配。没有super你不必担心这一点,而且代码的可读性要强得多。

  2. 其中一个类是 mixin。

    A mixin https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful是一个类designed与多重继承一起使用。这意味着我们不必手动调用两个父构造函数,因为 mixin 会自动为我们调用第二个构造函数。由于这次我们只需要调用一个构造函数,因此我们可以使用super以避免硬编码父类的名称。

    Example:

    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__()并传递它收到的任何参数。
    • 子类继承自mixinfirst: class FooBar(FooMixin, Bar)。如果基类的顺序错误,则 mixin 的构造函数将永远不会被调用。
  3. 所有基类都是为协作继承而设计的。

    为协作继承而设计的类很像 mixin:它们将所有未使用的参数传递给下一个类。像以前一样,我们只需调用super().__init__()并且所有父构造函数都将被链式调用。

    Example:

    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首先,代码仍然可以正常工作。但这只是正确的,因为所有参数都作为关键字参数传递。使用位置参数很容易导致参数的顺序错误,因此协作类通常只接受关键字参数。

    这也是我之前提到的规则的一个例外:两者CoopFoo and CoopBar继承自object,但他们仍然打电话super().__init__()。如果他们不这样做,就不会有合作继承。

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

构造函数是类的公共接口的一部分。如果该类被设计为 mixin 或用于协作继承,则必须进行记录。如果文档没有提到任何此类内容,则可以安全地假设该类isn't专为协作多重继承而设计。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用多重继承调用父类 __init__ ,正确的方法是什么? 的相关文章

随机推荐