web-dev-qa-db-ja.com

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

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

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__を記述するための2つの典型的なアプローチがあります。

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

ただし、どちらの場合でも、親クラス(AおよびB同じ規則に従わない場合、コードは正しく機能しません (一部が欠落する場合があります) 、または複数回呼び出されます)。

では、正しい方法は何ですか? 「一貫性を保って、どちらか一方に従うだけ」と言うのは簡単ですが、Aまたは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が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

Bのinitは呼び出されないことに注意してください。そのため、継承するクラスの初期化を知らない/制御しない限り(AおよびB)私が書いているクラス(C)に対して安全な選択をすることはできないようです。

130
Adam Parkin

あなたの質問に対する答えは、1つの非常に重要な側面に依存します:あなたのベースクラスは多重継承のために設計されていますか?

3つの異なるシナリオがあります。

  1. 基本クラスは、無関係のスタンドアロンクラスです。

    基本クラスが独立して機能できる個別のエンティティであり、互いに認識していない場合、それらは多重継承用に設計されていますnot。例:

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

    重要:FooBarsuper().__init__()!を呼び出さないことに注意してください。これが、コードが正しく機能しなかった理由です。 Pythonでのダイヤモンドの継承の仕組みにより、基本クラスがobjectであるクラスは、super().__init__()を呼び出してはなりません。お気づきのとおり、そうすると、object.__init__()ではなく別のクラスの__init__を呼び出すことになり、多重継承が壊れます。 免責事項:object- subclassesでsuper().__init__()を回避することは私の個人的な推奨事項であり、決して合意された合意ではありませんpythonコミュニティ。一部の人々はすべてのクラスでsuperを使用することを好み、クラスが期待どおりに動作しない場合は常に adapter と書くことができると主張します。)

    これはまた、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を使用する場合、クラスは dependency injection をサポートします。一方、間違いを犯しやすい。たとえば、FooBarの順序を変更する場合(class FooBar(Bar, Foo)など)、super呼び出しを一致するように更新する必要があります。 superがなければ、これについて心配する必要はなく、コードははるかに読みやすくなります。

  2. クラスの1つはmixinです。

    mixin は、多重継承で使用されるdesignedのクラスです。これは、ミックスインが自動的に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__()を呼び出し、受け取った引数をすべて渡します。
    • サブクラスは、mixinfirstから継承します:class FooBar(FooMixin, Bar)。基本クラスの順序が間違っている場合、mixinのコンストラクターは呼び出されません。
  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から継承することもできますが、コードは同じように機能します。しかし、すべての引数はキーワード引数として渡されるため、これは真実です。位置引数を使用すると、引数の順序を間違えやすくなるため、協調クラスではキーワード引数のみを受け入れるのが慣例です。

    これは、前述のルールの例外でもあります。CoopFooCoopBarは、objectを継承しますが、super().__init__()を呼び出します。そうしなかった場合、協力的な継承はありません。

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

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

32
Aran-Fey

この記事は、協調的な多重継承の説明に役立ちます。

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

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

公式pythonサイトの技術的な記事は次のとおりです。

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

3
Kelvin

どちらのアプローチ(「新しいスタイル」または「古いスタイル」)も機能しますAおよびBのソースコードを制御できる場合。そうでない場合は、アダプタークラスの使用が必要になる場合があります。

アクセス可能なソースコード:「新しいスタイル」の正しい使用

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

ここでは、A.__init__B.__init__が明示的に呼び出されるため、MROは重要ではありません。 class C(B, A):も同様に機能します。

このケースは、以前のように新しいスタイルの多重継承用に「設計」されていませんが、多重継承は依然として可能です。


ここで、ABがサードパーティライブラリのものである場合、つまりABのソースコードを制御できない場合簡単な答え:必要なsuper呼び出しを実装するアダプタークラスを設計し、空のクラスを使用してMROを定義する必要があります( Raymond Hettingerのsuperに関する記事 -特にセクション「組み込み方法非協力的なクラス」)。

サードパーティの親:Asuperを実装しません。 B

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を定義できるようにします。MROは、super(Adapter, self).__init__()の実行時に機能します。

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

サードパーティの親:Asuperを実装します。 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を最初に呼び出し、次に明示的な呼び出しを行います。サードパーティの親を持つ各ケースには、一意のアダプタークラスが必要であることに注意してください。

したがって、継承するクラスの初期化(AおよびB)を知らない/制御しない限り、作成中のクラス(C)に対して安全な選択をすることはできません。

アダプタクラスを使用して、AおよびBのソースコードを control しないケースを処理できますが、 know親クラスのinitがsuperを実装する方法(もしあれば)。

2
Nathaniel Jones

サードパーティライブラリのサブクラスクラスを乗算する場合、いいえ、ベースクラスのプログラミング方法に関係なく実際に動作するベースクラス__init__メソッド(または他のメソッド)を呼び出すブラインドアプローチはありません。

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

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

2
Ben

レイモンドが答えで言ったように、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
Jundiaius