多重継承で、特定のコンストラクタが呼ばれない
pythonで多重継承したとき、継承元の基本クラスの中に、super().__init__()
してないクラスが存在すると、インタプリタが MRO(メソッド解決順序)を追えなくなるので、多重継承の2つ目のコンストラクタが呼ばれないという現象が発生します。
たとえば、以下のようなケースでは、
class A:
def __init__(self):
print('>A', end='')
class B(A):
def __init__(self):
print('>B', end='')
super().__init__()
class C:
def __init__(self):
print('>C', end='')
super().__init__()
class D(C):
def __init__(self):
print('>D', end='')
super().__init__()
class E(B, D):
def __init__(self):
print('>E', end='')
super().__init__()
x=E()
# >>> E>B>A # 続きの >D>C が出てこない...
上記の例で、x=E()
を実行すると、E>B>A までしか表示されません。
本例の正解のパターンは、
class A:
def __init__(self):
print('>A', end='')
super().__init__() # <- これが必要。
# 実は objectの__init__() が呼ばれるわけではなく、class D の__init__() が呼ばれる!
class B(A):
def __init__(self):
print('>B', end='')
super().__init__()
class C:
def __init__(self):
print('>C', end='')
super().__init__()
class D(C):
def __init__(self):
print('>D', end='')
super().__init__()
class E(B, D):
def __init__(self):
print('>E', end='')
super().__init__()
x=E()
# >>> E>B>A>D>C
これに気づくのに、数日の時間を浪費ました。
python インタプリタは、実行時には、super().__init__()
の呼び出しによってMRO順序を元に親のインスタンスを辿り、オブジェクトを初期化して行くという動きをします。
super().__init__()
の super()
が、MROを検索するという動きをするからです。
具体的な処理の流れは以下のようになります。
順序 | クラス | クラスに保持されるMRO順序 | クラスのsuper().__init__() が呼び出すメソッド |
---|---|---|---|
1. | E | E>B>A>D>C>object | クラスBの __init__(self)
|
2. | B | B>A>D>C>object | クラスAの __init__(self)
|
3. | A | A>D>C>object | クラスDの __init__(self)
|
4. | D | D>C>object | クラスCの __init__(self)
|
5. | C | C>object | 暗黙のobjectクラスの def __init__(self)
|
前例の test_ng.py
では、上記3番の、super().__init__()
の呼び出しが無かったために、
クラスDの __init__(self)
が呼ばれず、処理が滞っていたということです。
なお、MROの並びは、
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>)
# または
>>> E.mro()
(<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>)
# または、
# 型Eが持つMRO順序のうち型Eからリストアップする
>>> type(E).mro(E)
[<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>]
>>>
で確認することができます。
ベースクラスのコンストラクタに引数があるようなケース
滅多にないとは思いますが、下記の例のような実装では、それぞれのコンストラクタの引数が異なるような場合、エラーになります。
class A:
def __init__(self, name1, name2):
print(f'>A("{name1}", "{name2}")', end='')
super().__init__()
class B:
def __init__(self, name3):
print(f'>B("{name3}")', end='')
super().__init__()
class E(B, A):
def __init__(self):
print('>E', end='')
super(E, self).__init__("name3")
super(B, self).__init__("name1", "name2")
# >>> x=E()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 4, in __init__
# File "<stdin>", line 4, in __init__
# TypeError: __init__() missing 2 required positional arguments: 'name1' and 'name2'
# >E>B("name3")>>>
super().__init__()
を呼び出す限り、MROの検索が行われ上位へ処理が進んでしまうため、引数の数の相違がおきてしまいエラーとなります。
このケースでは、MROの検索が先に進まない様に、super().__init__()
を止めればエラーは出ません。しかし、本末転倒的なやり方であり適切とは言えません。デフォルトのパラメータを使用するなど、引数の取り方を工夫するなどして回避策を考えるべきと思います。
class A:
def __init__(self, name1, name2):
print(f'>A("{name1}", "{name2}")', end='')
#super().__init__()
class B:
def __init__(self, name3):
print(f'>B("{name3}")', end='')
#super().__init__()
class E(B, A):
def __init__(self):
print('>E', end='')
super(E, self).__init__("name3")
super(B, self).__init__("name1", "name2")
# >>> x=E()
# >E>B("name3")>A("name1", "name2")>>>
super() メソッドの引数
super()
メソッドには引数があります。
長年pythonに携わってきた方々には当たり前かもしれませんが、Pythonドキュメントには、具体的な説明がなく、代わって長々とMROとやらの素晴らしさについて語られていました。
super()
メソッドの引数については、
C で書かれた Python のソースを見ると、およそ以下の説明の通りとなります。
- 第1引数は、MRO上で検索を始める前の型(クラス)を指定します
MRO順序が A>B>C>D>E のとき、B から検索をしたいのであれば、A を指定します。 - 第2引数は、MROを保持するオブジェクトのインスタンスを指定します
通常は、オブジェクト自身のスーパークラスにアクセスすることが目的なので self を指定するのが適切です。super() は、第2引数のオブジェクトの型を取得し、そこからMROリストを得て、第1引数に指定した型を検索し、その次の型から、親クラスを決定します。
本節のまとめ
MROの順序決定アルゴリズムは、C3線形アルゴリズム という様です。
そもそも、コード自体は後々動的に変わることはないのに、何故に、動的な動作による解決という手段を取り入れたのか? … ただ、コンパイラではないので、そこが動的でないと難しいのかもしれません。構文というものが存在するのだから、出来ないことはないと思う、と言うと、google の Gemini さんにも、さんざん諭されてしまいました。
皆様にお叱りを受けてしまうかもしれないけれども、30年前の javascipt や C++ が、驚くべき進化を遂げたように、pythonもいずれ進化して、MROなどというものを理解していなくても、多重継承が楽にできることに期待したいです。それまでに私が生きれいれば!!