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

多重継承で親クラス __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の書き方には、2 つの典型的なアプローチがあります__init__

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

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

では、正しい方法は何でしょうか? 「一貫性を保ち、どちらか一方に従う」と言うのは簡単ですが、 またはABサードパーティのライブラリからのものである場合はどうでしょうか? すべての親クラスのコンストラクターが呼び出されるようにする (正しい順序で、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

の init は決して呼び出されないことに注意してください。したがって、(および)Bから継承するクラスの init を知ったり制御したりしない限り、記述しているクラス ( ) に対して安全な選択を行うことはできないようです。ABC

ベストアンサー1

質問に対する答えは、1 つの非常に重要な側面によって決まります。基本クラスは多重継承用に設計されていますか?

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

  1. 基本クラスは関連のないスタンドアロン クラスです。

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

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

    重要:も もを呼び出していFooないことに注意してください。これが、コードが正しく動作しなかった理由です。Python でのダイヤモンド継承の仕組みにより、をベースクラスとするクラスは を呼び出すべきではありません。お気づきのとおり、これを行うと、ではなく別のクラスの を呼び出すことになるため、多重継承が壊れます。(免責事項:サブクラスでの の使用を避けることは私の個人的な推奨事項であり、Python コミュニティで合意されたコンセンサスではありません。すべてのクラスで を使用することを好む人もいます。常に を記述できると主張しています。Barsuper().__init__()objectsuper().__init__()__init__object.__init__()super().__init__()objectsuperアダプタクラスが期待どおりに動作しない場合。

    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、クラスは依存性注入一方で、間違いを起こしやすくなります。たとえば、Fooとの順序を変更する場合Bar( のように)、一致するように呼び出しをclass FooBar(Bar, Foo)更新する必要があります。 がなければ、この点について心配する必要がなく、コードがはるかに読みやすくなります。supersuper

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

    混入しますは多重継承で使用するために設計されたクラスです。つまり、ミックスインが自動的に 2 番目のコンストラクターを呼び出すため、両方の親コンストラクターを手動で呼び出す必要はありません。今回は 1 つのコンストラクターのみを呼び出す必要があるため、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__()。 そうでなければ、協調継承は行われません。

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

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

おすすめ記事