多重継承で親クラス__init__を呼び出す、正しい方法は何ですか?


174

多重継承シナリオがあるとしましょう:

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?

書き込みには、2つの典型的なアプローチがありますCのは__init__

  1. (旧式) ParentClass.__init__(self)
  2. (新しいスタイル) super(DerivedClass, self).__init__()

ただし、どちらの場合でも、親クラス(ABが同じ規則に従っていない場合、コードは正しく機能しません(一部が見つからないか、複数回呼び出される可能性があります)。

では、正しい方法は何でしょうか?「ただ一貫している、どちらか一方に従っている」と言うのは簡単ですが、サードパーティのライブラリからのものである場合、AまたはBサードパーティのライブラリからのものである場合はどうでしょうか。すべての親クラスのコンストラクターが(正しい順序で、1回だけ)呼び出されるようにする方法はありますか?

編集:私がそうしている場合、私が何を意味するかを確認するには:

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

Binitが2回呼び出されることに注意してください。私が行った場合:

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

Binitが呼び出されることは決してないことに注意してください。したがって、継承元のクラスのinitを知っている/制御しない限り(AおよびB)、作成しているクラスに安全な選択をすることができないようです(C)。


回答:


78

どちらの方法でも問題なく機能します。を使用するアプローチsuper()により、サブクラスの柔軟性が向上します。

直接呼び出しのアプローチでC.__init__は、A.__init__およびの両方を呼び出すことができますB.__init__

使用する場合はsuper()、クラスが協力、複数の継承のために設計する必要がどこC通話super呼び出し、A「また呼ぶのコードsuperどの呼び出すBのコード」。で何ができるかについての詳細は、http://rhettinger.wordpress.com/2011/05/26/super-considered-superを参照してくださいsuper

[後で編集する回答の質問]

したがって、継承するクラスのinitを知っている/制御しない限り(AおよびB)、作成しているクラス(C)を安全に選択できないようです。

参照記事では、Aおよびの周りにラッパークラスを追加して、この状況を処理する方法を示していますB。「非協力的なクラスを組み込む方法」というタイトルのセクションに、うまく機能した例があります。

多重継承がより簡単になり、CarクラスとAirplaneクラスを簡単に作成してFlyingCarを取得できることを望むかもしれませんが、実際には、個別に設計されたコンポーネントは、私たちが望むようにシームレスに組み合わせる前にアダプターまたはラッパーを必要とすることがよくあります:-)

もう1つの考え:複数の継承を使用して機能を構成することに不満がある場合は、構成を使用して、どのメソッドがどの機会に呼び出されるかを完全に制御できます。


4
いいえ、ありません。Bのinitがsuperを呼び出さない場合、super().__init__()アプローチを実行してもBのinitは呼び出されません。私が呼び出した場合A.__init__()B.__init__()、直接、その後、(AとBが通話を行う場合super)私は複数回呼び出されているBのinitを取得します。
アダムパーキン

3
@AdamParkin(編集した質問について):親クラスの1つがsuper()で使用するように設計されていない場合は、通常、super呼び出しを追加する方法でラップできます。参照された記事は、「非協力的なクラスを組み込む方法」というタイトルのセクションでうまく機能した例を示しています。
レイモンドヘッティンガー2012年

1
どういうわけか私は記事を読んだときにそのセクションを逃した。 まさに私が探していたもの。ありがとう!
アダムパーキン

1
python(できれば3!)を記述していて、特に複数の継承を使用している場合は、rhettinger.wordpress.com / 2011/05/26 / super-considered-superを読む必要があります。
Shawn Mehan 2017

1
賛成票を投じたのは、現時点で空車があると確信していたのに、なぜ空飛ぶ車がないのかがわかったからです。
msouth

65

あなたの質問への答えは、1つの非常に重要な側面に依存します。基本クラスは多重継承用に設計されていますか?

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__()免責事項:回避super().__init__()object-subclassesは私の個人的な勧告で、決して合意されたPythonコミュニティにおけるコンセンサス一部の人々が使用することを好む。superあなたはいつも書くことができると主張し、すべてのクラスでは、アダプタをクラスとして動作しない場合期待します。)

    これobjectは、__init__メソッドを継承せず、メソッドも持たないクラスを記述してはならないことも意味します。__init__メソッドをまったく定義しないと、を呼び出すのと同じ効果がありsuper().__init__()ます。クラスがから直接継承する場合は、次のobjectように空のコンストラクタを追加してください。

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

    とにかく、この状況では、各親コンストラクターを手動で呼び出す必要があります。これを行うには2つの方法があります。

    • なし 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

    これら2つの方法には、それぞれ独自の長所と短所があります。を使用するsuper場合、クラスは依存性注入をサポートします。一方、間違いを犯しやすいです。たとえばFooBar(のようにclass FooBar(Bar, Foo))の順序を変更した場合super、一致するように呼び出しを更新する必要があります。なければsuper、あなたはこれについて心配する必要はありませんし、コードがはるかに読みやすいです。

  2. クラスの1つはミックスインです。

    ミックスインはだクラスで設計された多重継承で使用します。つまり、mixinが自動的に2番目のコンストラクターを呼び出すため、両方の親コンストラクターを手動で呼び出す必要はありません。今回は単一のコンストラクターを呼び出すだけなので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.

    ここで重要な詳細は次のとおりです。

    • ミックスインはsuper().__init__()、受け取った引数を呼び出して渡します。
    • サブクラスは最初にミックスインから継承しますclass FooBar(FooMixin, Bar)。基本クラスの順序が間違っている場合、ミックスインのコンストラクターが呼び出されることはありません。
  3. すべての基本クラスは、協調的な継承のために設計されています。

    協調的な継承のために設計されたクラスは、ミックスインによく似ています。未使用のすべての引数を次のクラスに渡します。前と同様に、呼び出すだけで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最初から継承することもできますが、コードは同じように機能します。しかし、すべての引数がキーワード引数として渡されるため、それは本当です。位置引数を使用すると、引数の順序が間違って取得されやすくなるため、協調型クラスはキーワード引数のみを受け入れるのが通例です。

    これは、前述のルールの例外でもあります。両方CoopFooとはCoopBarから継承されますobjectが、それらはまだを呼び出しますsuper().__init__()。そうでない場合、協調的な継承はありません。

結論:正しい実装は、継承元のクラスによって異なります。

コンストラクターは、クラスのパブリックインターフェイスの一部です。クラスがミックスインとして、または協調継承のために設計されている場合は、それを文書化する必要があります。ドキュメントでそのようなことについて何も言及されていない場合は、クラス協調的多重継承用に設計されていないと想定しても安全です。


2
あなたの第二のポイントは私を吹き飛ばしました。ミックスインしているクラスが期待する属性を持っているかどうかを確認できないため、実際のスーパークラスの右側にあるMixinを見ただけで、かなり緩くて危険だと思いました。将軍super().__init__(*args, **kwargs)をミックスインに入れて、最初にそれを書くことを考えたことはありませんでした。それはとても理にかなっています。
Minix

10

およびのソースコードを制御できる場合は、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チェーンに沿って続行C.__init__B.__init__ます。
  • super(B, self).__init__()で開始されたMROチェーンに沿って続行C.__init__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つまり、あなたはAandのソースコードを制御できませんBか?短い答え:必要なsuper呼び出しを実装するアダプタークラスを設計し、空のクラスを使用してMROを定義する必要があります(レイモンドヘッティンガーの記事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

クラスは、MROを定義できるようにAdapter実装されます。MROは、実行時に機能します。superCsuper(Adapter, self).__init__()

そして、それが逆の場合はどうなりますか?

サードパーティの親:A実装super; Bではない

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を知っている/制御しない限り(AおよびB)、作成しているクラスに安全な選択をすることができないようです(C)。

のソースコードを制御しない場合ABアダプタークラスを使用することで対処できますが、そうするためには、親クラスのinitが(もしあれば)どのように実装されているかを知っている必要がありますsuper


4

レイモンドが彼の答えで言ったように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")

1
最良の答えは私見です。これは、より将来性があるため、特に役立ちました
Stephen Ellwood

3

この記事は、協調多重継承について説明するのに役立ちます。

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

mro()メソッド解決の順序を示す便利なメソッドについて説明しています。2番目の例では、を呼び出すsuperAsuperMROで呼び出しが続行されます。順序の次のクラスはですB。これが、Bのinitが初めて呼び出される理由です。

これは公式のpythonサイトからのより技術的な記事です:

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


2

サードパーティのライブラリからクラスをサブクラス化する場合、いいえ、__init__基本クラスのプログラム方法に関係なく実際に機能する基本クラスメソッド(またはその他のメソッド)を呼び出すブラインドアプローチはありません。

superクラス作成者が知る必要のない複雑な多重継承ツリーの一部として、メソッドを協調的に実装するように設計されたクラスを作成することができます。ただし、を使用する場合と使用しない場合がある任意のクラスから正しく継承するためにそれを使用する方法はありませんsuper

基本的に、クラスが基本クラスを使用して、superまたは直接呼び出しでサブクラス化されるように設計されているかどうかは、クラスの「パブリックインターフェイス」の一部であるプロパティであり、そのようにドキュメント化する必要があります。ライブラリの作成者が予想した方法でサードパーティのライブラリを使用していて、ライブラリに適切なドキュメントがある場合、通常、特定のことをサブクラス化するために何が必要かがわかります。そうでない場合は、サブクラス化するクラスのソースコードを調べて、それらの基本クラスの呼び出し規約を確認する必要があります。ライブラリの作成者予期していなかった方法で、1つ以上のサードパーティライブラリの複数のクラスを組み合わせる場合、スーパークラスのメソッドを一貫して呼び出すことがまったくできない可能性があります。; クラスAが使用する階層の一部であり、superクラスBがスーパーを使用しない階層の一部である場合、どちらのオプションも機能することが保証されていません。特定のケースごとに機能する戦略を理解する必要があります。


@RaymondHettingerさて、あなたはすでにそれを考えてあなたの回答に記事を書いてリンクしているので、それに追加することはあまりないと思います。:)しかし、スーパーを使用しないクラスをスーパー階層に一般的に適合させることは可能ではないと思います。関係する特定のクラスに合わせたソリューションを考え出す必要があります。
ベン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.