(2019.10) Web上はいまだに Python2 のページが多いため、Python3 の挙動を整理するメモ。
Python は多重継承ができる。継承の経路で共通のスーパクラスがある場合は、必ず菱形継承になる。
コンストラクタの呼出しは、かなり難しい挙動になっている。そのため、多重継承したい場合、運用上の注意がいくつかある。Mix-in のように実装するのがいい。
次の例は、A
クラスで菱形になる。
サブクラスで定義したメソッドは、スーパクラスのをoverrideする。class C
のfunc()
のほうが優先される。
メソッド検索の優先順位は mro()
クラスメソッドで表示できる。
実行結果:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
Python3では、継承によるメソッド検索は, 左優先、深さ優先でおこなわれる。
菱形の部分は、スーパクラスが後回しになる。class A
は, class C
からも継承されているため、C
の後ろになっている。
インスタンス変数 (メンバ) は、インスタンス全体で共用される。継承関係にあるクラス同士や, 相互に継承されなくてもとにかく継承経路のなかにあるクラス同士で, メンバの名前が被ると同じ変数にアクセスしてしまう。発見しづらいバグの温床。
次の例は、class B
のメンバをclass C
のメソッド内で上書きしてしまう。
継承されることがありうるクラスでは、プライベート変数を使わなければならない。プライベート変数は、変数名を '__
' (アンダースコア2つ) で始める。クラスごとに領域が確保される。
実は Pythonのプライベート変数は本物のプライベート変数ではないが、実用上はこれで足りる。なお、Python に protected 変数はない。
次の例は, C#f3()
内で __x
に代入しているが、これは B
クラスの __x
とは別になる。ので、上書きされない。
実行結果:
あいう
super()
Pythonは、あるクラスのインスタンス化 (オブジェクトの生成) 時に呼び出されるのは、そのクラスのコンストラクタ (構築子) のみ。スーパクラスのコンストラクタが自動的に呼出されない。びっくり仕様!! サブクラスのコンストラクタから、明示的に呼び出さなければならない。
Web上の解説では、これが混乱していて、嘘とは言わないまでも、不正確なものがあまりに多い。
Python のコンストラクタは __init__
という名前のメソッド。通常のメソッドと書き方に違いはない. super().__init__()
でスーパクラスのメソッドを呼び出す, ことになっている。
しかし、正しくは、Method Resolution Order (MRO) の順序で, 次のクラスのメソッドが呼び出される。多重継承の場合は、スーパクラスのコンストラクタを呼び出すとは限らない。見ず知らずの隣のクラスのメソッドを呼び出すことがある。
次の例では、A#__init__()
内の super()
は、class B
を表す。これは非常に不味い。何しろ、知らないクラスなので、呼び出す際の実引数として何を与えるべきか、決められない。
サブクラスから見ると、一番左以外のスーパクラスのコンストラクタに、値を渡すことができない。
実行結果:
[<class '__main__.Derived'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>] Derived: call superclass A: call superclass B: call superclass Base: call superclass Base::__init__ B::__init__ A::__init__ Derived::__init__ <__main__.Derived object at 0x7fa667d36550> 333 333
上の例では、作為的に、コンストラクタの仮引数の個数を変えてみた。
長々と書いてきたが、この Python の挙動は、Common Lisp の call-next-method
関数と同じ。菱形継承になる、継承関係に基づくメソッド呼出し順序で単純な関数呼出しが行われるのも同じ。
Common Lisp でクラス間のメソッド呼出し順序を得るのは class-precedence-list
関数だが、上の Python の mro()
と同じになる。
多重継承がそもそも状況を難しくしているが、多重継承は強力な機能なので、使わないわけにはいかない。
他人が作ったクラス AA
と、さらに別のクラス BB
を多重継承するときに、AA
のコンストラクタは変更できないので、うまく動かないのはどうしようもない. (このケースの対策は後述).
まず、自分でクラスを作る場合の対策を考える。
多重継承するときは、一つのクラス (一番左) 以外は mix-in として動くように、定義すればいい。
Mix-in として定義するクラスでは, コンストラクタを定義しない。定義する場合も、引数はすべて次のクラスへ toss する。何しろ、その引数は、呼出し元のクラスのスーパクラスのためのものであって、自クラスのものではない。
あたかも単一継承で、順番にスーパクラスのコンストラクタが呼び出されていくようにする。
複数のスーパクラスに適切な引数を渡すことができないのが問題なので、super()
を使わずにスーパクラス名を直接指定すると、よりよい。スーパクラス・サブクラスの関係にないクラスのメソッドが呼び出されてしまう問題が起こらない。
スーパクラスの中身を変更できない場合もOK.
次の例は、A
から B
の呼出しが発生しない。
実行結果:
Derived: call superclass A: call superclass Base: call superclass Base::__init__ A::__init__ B: call superclass Base: call superclass Base::__init__ B::__init__ Derived::__init__ <__main__.Derived object at 0x7f8c6215b950> 333
しかし、この方法では、菱形継承だが、Base
クラスのコンストラクタが2回呼出される。
解決策は、まずは、コンストラクタを複数回呼び出されてもいいようにする、か。ただ、筋がよくない。
よりよいのは、多重継承している側の Derived#__init__()
内から B#__init__()
の呼出しを削除する。Mix-in class である B
では、コンストラクタを定義しないか、定義したとしても実引数は次のクラスへそのまま流す。従い、このコンストラクタは呼び出すべきではない。