多重継承を行う場合には、クラス定義時に基底クラスとなるクラスを列挙していく。以下に例を示す(クラス名やメソッド名は上の例よりもシンプルにした)。
class B:
def m(self):
print("m on B")
class D1(B):
def m(self):
print("m on D1")
class D2(B):
def m(self):
print("m on D2")
class D3(D1, D2):
pass
d3 = D3()
d3.m()
これは典型的なダイヤモンド継承だ。
ダイヤモンド継承においては、メソッド呼び出しをどう解決するかが問題になる(Python 3ではobjectクラスをルートとしたクラス階層の中に全てのクラス/オブジェクトが含まれるため、上のコードのようにしなくとも、おのずとダイヤモンド継承にまつわる問題が発生する)。D3クラスではインスタンスメソッドmを定義(オーバーライド)していないが、これを呼び出すとどのメソッドが呼び出されるだろうか。
>>> class B:
... def m(self):
... print("m on B")
...
>>> class D1(B):
... def m(self):
... print("m on D1")
...
>>> class D2(B):
... def m(self):
... print("m on D2")
...
>>> class D3(D1, D2):
... pass
...
>>> d3 = D3()
>>> d3.m()
m on D1
ここでは、D1クラスのメソッドが呼び出された。次にもう一度、実験をしてみる。D3クラスの定義での基底クラス指定を「(D2, D1)」にした以外は同じものを実行した結果を以下に示す。
…… 省略 ……
>>> class D3(D2, D1):
... pass
...
>>> d3 = D3()
>>> d3.m()
m on D2
基底クラスの指定順序を変えると、呼び出されるメソッドも変化するということだ。
どのメソッドが呼び出されるのかは、クラス階層をさかのぼってメソッドや属性を探索する順序による。詳細はPythonにおける「新スタイルクラス」(Python 3のクラスはこのスタイルのクラス)のメソッド解決順序(MRO: Method Resolution Order)を定めた「The Python 2.3 Method Resolution Order」が詳しいのだが、こまごまとした規則を覚えていなくともクラスオブジェクトの__mro__属性を使えば、これを取得できる(MROは「深さ優先、左から右、の順番で検索をするが、検索ルートの中で特定のクラスが複数回出てきた場合には後回しとなるよう」に基本的には決定される。「後回し」とは複数ある直接基底クラスが共通する基底クラスを持っていた場合、その基底クラスは直接基底クラスよりも検索順が後になるということだ。このため、一見すると幅優先に近い順序で検索が行われるように見える。ただし、実際にはより複雑な処理が行われているようだ)。
例えば、変更前のクラスの「D3.__mro__」属性の値は次のようになる。
>>> D3.__mro__
[<class '__main__.D3'>, <class '__main__.D1'>, <class '__main__.D2'>,
<class '__main__.B'>, <class 'object'>]
これに対して、基底クラスの指定を「(D2, D1)」に変更したD3クラスでは「D3.__mro__」属性の値は次のようになる。
>>> class D3(D2, D1):
... pass
...
>>> D3.mro()
[<class '__main__.D3'>, <class '__main__.D2'>, <class '__main__.D1'>,
<class '__main__.B'>, <class 'object'>]
また、組み込み関数superを使うと多重継承時のメソッド探索をカスタマイズできる(単一継承時には上でも見たようにオーバーロードされたメソッドから基底クラスのメソッドを呼び出すのに使える。ただし、単一継承時でもMROによって検索が行われることには変わりはない。多重継承ではMROの決定が単一継承よりも複雑になるということだ)。例えば、上のD3クラスは次のように変更できる。
…… 省略 ……
class D3(D1, D2): # 基底クラスの指定はD1、D2の順序
def m(self):
super(D3, self).m()
d3 = D3()
d3.m()
Pythonのライブラリレファレンスによれば、この形式の組み込み関数superは「メソッドの呼び出しを type の親または兄弟クラスに委譲するプロキシオブジェクトを」返す(typeは第1引数。この場合はD3)。つまり、D3の親か兄弟クラスのインスタンスメソッドmを呼び出すということだ。第2引数は、インスタンスメソッド呼び出しで使われるコンテキスト(オブジェクト)だ。つまり、D3クラスのインスタンスに対して、親クラスまたは兄弟クラスのインスタンスメソッドmを呼び出すという意味になる。なお、引数を省略した「super().m()」は「super(D3, self).m()」と同じ意味になる。
この場合にはD3.__mro__属性の値に従ってメソッドの検索はD1→D2→Bの順に行われる(「親または兄弟クラス」という点に注意。D3自体は検索対象からは外れる)。よって、「d3.m()」呼び出しの結果は次のようになる。
>>> d3 = D3()
>>> d3.m()
m on D1
今度はD3クラスを次のように変更してみる。
class D3(D1, D2):
def m(self):
super(D1, self).m()
d3 = D3()
d3.m()
実行結果は次のようになる。
>>> d3 = D3()
>>> d3.m()
m on D2
ソースコードまで追えなかったので推測になるが、これにより、D3.__mro__属性の値でD1よりも後ろにあるもの(つまり、親または兄弟となるクラス)に対してメソッド解決が行われるようになると思われる。
多重継承を行った場合のメソッド呼び出しの解決はプログラマーの頭を悩ませるものだが、Pythonでは明確なルール(MRO)とそのカスタマイズ手段が与えられている。とはいえ、シンプルさを重要視するのであれば、本稿では触れられなかったがミックスインなどの手法を採用するのがよいかもしれない。
本稿ではPythonのクラスについてざっくりと一巡りしてきた。次回はモジュールについて見ていく予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.