多重継承, コンストラクタ呼出し順序 [Python3]

(2019.10) Web上はいまだに Python2 のページが多いため、Python3 の挙動を整理するメモ。

ポイント

Python は多重継承ができる。継承の経路で共通のスーパクラスがある場合は、必ず菱形継承になる。

コンストラクタの呼出しは、かなり難しい挙動になっている。そのため、多重継承したい場合、運用上の注意がいくつかある。Mix-in のように実装するのがいい。

菱形継承

次の例は、Aクラスで菱形になる。

Python
[RAW]
  1. class A:
  2. def func(self): pass
  3. class B(A): pass
  4. class C(A):
  5. def func(self): pass
  6. class D(B, C): pass
  7. print(D.mro())

サブクラスで定義したメソッドは、スーパクラスのをoverrideする。class Cfunc() のほうが優先される。

メソッド検索の優先順位は 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 のメソッド内で上書きしてしまう。

Python
[RAW]
  1. class A: pass
  2. class B(A):
  3. def f2(self):
  4. self.x = 'あいう'
  5. class C(A):
  6. def f3(self):
  7. self.x = 'hoge' # スーパクラスですらないクラスのメンバを上書き
  8. class D(B, C):
  9. def get_x(self):
  10. return self.x
  11. obj = D()
  12. obj.f2()
  13. print(obj.get_x()) #=> あいう
  14. obj.f3()
  15. print(obj.get_x()) #=> hoge

プライベート変数

継承されることがありうるクラスでは、プライベート変数を使わなければならない。プライベート変数は、変数名を '__' (アンダースコア2つ) で始める。クラスごとに領域が確保される。

実は Pythonのプライベート変数は本物のプライベート変数ではないが、実用上はこれで足りる。なお、Python に protected 変数はない。

次の例は, C#f3() 内で __x に代入しているが、これは B クラスの __x とは別になる。ので、上書きされない。

Python
[RAW]
  1. class B():
  2. def f2(self):
  3. self.__x = 'あいう'
  4. def get_x(self):
  5. return self.__x
  6. class C():
  7. def f3(self):
  8. self.__x = 'hoge'
  9. class D(B, C): pass
  10. obj = D()
  11. obj.f2()
  12. obj.f3()
  13. print(obj.get_x())

実行結果:

あいう

コンストラクタの呼出し経路: super()

Pythonは、あるクラスのインスタンス化 (オブジェクトの生成) 時に呼び出されるのは、そのクラスのコンストラクタ (構築子) のみ。スーパクラスのコンストラクタが自動的に呼出されない。びっくり仕様!! サブクラスのコンストラクタから、明示的に呼び出さなければならない。

Web上の解説では、これが混乱していて、嘘とは言わないまでも、不正確なものがあまりに多い。

Python のコンストラクタは __init__ という名前のメソッド。通常のメソッドと書き方に違いはない. super().__init__() でスーパクラスのメソッドを呼び出す, ことになっている。

しかし、正しくは、Method Resolution Order (MRO) の順序で, 次のクラスのメソッドが呼び出される。多重継承の場合は、スーパクラスのコンストラクタを呼び出すとは限らない。見ず知らずの隣のクラスのメソッドを呼び出すことがある。

次の例では、A#__init__() 内の super() は、class B を表す。これは非常に不味い。何しろ、知らないクラスなので、呼び出す際の実引数として何を与えるべきか、決められない。

サブクラスから見ると、一番左以外のスーパクラスのコンストラクタに、値を渡すことができない。

Python
[RAW]
  1. # 多重継承の経路に同じクラスがあった場合, 必ず菱形になる。
  2. class Base(object):
  3. def __init__(self, x):
  4. print('Base: call superclass')
  5. super().__init__()
  6. print( 'Base::__init__' )
  7. self.__xx = x
  8. def base_x(self):
  9. return self.__xx
  10. class A(Base):
  11. def __init__(self, bar, xx, ff):
  12. print('A: call superclass')
  13. super().__init__('hoge', 123) # これ, B#__init__() を呼び出している!
  14. # まったく関係ないクラスの仮引数に依存.
  15. print( 'A::__init__' )
  16. def s1(self):
  17. return super().base_x()
  18. class B(Base):
  19. def __init__(self, foo, xx): # 引数二つ
  20. print('B: call superclass')
  21. super().__init__(333)
  22. print( 'B::__init__' )
  23. def s2(self):
  24. return super().base_x()
  25. # 多重継承
  26. class Derived(A, B):
  27. def __init__(self, x): # 引数一つ
  28. print('Derived: call superclass')
  29. # スーパクラスのコンストラクタは, 自動的に呼び出されない. 何だって!!
  30. super().__init__("hoge", 500, -2.0)
  31. print( 'Derived::__init__' )
  32. print(Derived.mro())
  33. obj = Derived(10)
  34. print(obj) #=> <__main__.Derived object at 0x7f278c66dfd0>
  35. print(obj.s1(), obj.s2()) #=> 333 333 多重継承では、自動的に菱形になる.

実行結果:

[<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 の呼出しが発生しない。

Python
[RAW]
  1. # 多重継承の経路に同じクラスがあった場合, 必ず菱形になる。
  2. class Base(object):
  3. def __init__(self, x):
  4. print('Base: call superclass')
  5. super().__init__()
  6. print( 'Base::__init__' )
  7. self.__xx = x
  8. def base_x(self):
  9. return self.__xx
  10. class A(Base):
  11. def __init__(self, bar):
  12. print('A: call superclass')
  13. Base.__init__(self, 1250)
  14. print( 'A::__init__' )
  15. def s1(self):
  16. return super().base_x()
  17. class B(Base):
  18. def __init__(self, foo, xx):
  19. print('B: call superclass')
  20. Base.__init__(self, 333)
  21. print( 'B::__init__' )
  22. # 多重継承
  23. class Derived(A, B):
  24. def __init__(self, x):
  25. print('Derived: call superclass')
  26. # 各スーパクラスのコンストラクタを直接呼び出す.
  27. A.__init__(self, "hoge")
  28. B.__init__(self, "fuga", 300) # 菱形だが, Base ctor が2回呼出される.
  29. print( 'Derived::__init__' )
  30. obj = Derived(10)
  31. print(obj) #=> <__main__.Derived object at 0x7f278c66dfd0>
  32. print(obj.s1()) #=> 333 後から呼出した 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 では、コンストラクタを定義しないか、定義したとしても実引数は次のクラスへそのまま流す。従い、このコンストラクタは呼び出すべきではない。