そこで、まずは先ほどのコードを次のように修正しよう。
class B:
def __init__(self):
self.b_value = 'B'
print('class B init')
class C:
def __init__(self):
self.c_value = 'C'
print('class C init')
class D(C, B):
def __init__(self):
print('class D init')
super().__init__()
これはobjectクラスをダイヤモンドの頂点としてBクラスとCクラスがそれを継承して、DクラスはCクラスとBクラスを継承するというものだ。そして、Dクラスの__init__メソッドは「基底クラスの__init__メソッド」を呼び出すようにしている。
では、以下のコードでDクラスの動作を見てみよう。
d = D()
print(D.__mro__)
このコードを実行すると、次のような結果になる。
上の出力の通り、MROは「D→C→B→object」となるので、super関数を経由してCクラスの__init__メソッドが呼び出されることは分かるだろう。そのため、これでは先ほどと同様にCクラスの__init__メソッドに書かれたインスタンス変数c_valueの初期化しか行われない。そうではなく、Bクラスを含む全てのクラスで初期化したいので、ここでは__init__メソッドの連鎖が必要だ。これには、次のようにCクラスとBクラスの__init__メソッドでも「super().__init__()」呼び出しを書けばよい。
class B:
def __init__(self):
self.b_value = 'B'
print('class B init')
super().__init__()
class C:
def __init__(self):
self.c_value = 'C'
print('class C init')
super().__init__()
class D(C, B):
def __init__(self):
print('class D init')
super().__init__()
この状態で、以下のコードを実行してみよう。
d = D()
実行結果を以下に示す。
「D→C→B」というMROに記載された順序で__init__メソッドが呼び出されて、初期化が行われたことが分かる。
だが、このコードではBクラスとCクラスには何の関係もない。Cクラスの__init__メソッドで「super().__init__()」と書けば、それはobjectクラスの__init__メソッドを呼び出すようにも思える。そうではなく、実際にはこれはMROにリストされた順にメソッドを連鎖的に呼び出す仕組みとなっている。よって、「D→C→B→object」の順で__init__メソッドが呼び出されるようになる。クラスを継承する際には、それが単一継承であっても多重継承であっても、__init__メソッドでの初期化の連鎖は忘れないようにしよう。
先ほどの例はダイヤモンド継承を行っている場合だが、以下のようにobjectクラスではなく、Aクラスとその派生クラスであるBクラス、objectクラスを基底クラスとするCクラスから、Dクラスを作成してみよう(先ほども見た形)。
また、ここでは__init__メソッドではなく、再度helloメソッドを例として、4つのクラスで定義(またはオーバーライド)する。このとき、AクラスとCクラスではその基底クラスであるobjectクラスにhelloメソッドがないので、「super().hello()」呼び出しは書かずに、BクラスとDクラスのhelloメソッドでのみ「super().hello()」呼び出しを書くことにする。
class A:
def hello(self):
print('Hello from A')
class B(A):
def hello(self):
print('Hello from B')
super().hello()
class C:
def hello(self):
print('Hello from C')
class D(B, C):
def hello(self):
print('Hello from D')
super().hello()
この場合、MROが「D→B→A→C(→object)」となるのは既にお分かりだろう。このときに、以下のコードを試すとどうなるだろう。
d = D()
print(D.__mro__)
d.hello()
結果は次のようになる。
MROに従って、「D→B→A」の順にsuper関数を介してhelloメソッドの呼び出しが連鎖するが、Cクラスのhelloメソッドは呼び出されなかった。だが、実際には、DクラスのhelloメソッドからはCクラスのメソッドを呼び出したいこともあるかもしれない。これを実現する簡単な方法は以下のように、クラスを明記してしまうことだ。
class D(B, C):
def hello(self):
print('Hello from D')
C.hello(self)
これで先ほどと同じコードを実行してみよう。
d = D()
print(D.__mro__)
d.hello()
実行すると、結果は次のようになる。
この通り、MRO(D→B→A→C→object)とは異なり、DクラスのhelloメソッドからCクラスのhelloメソッドが呼び出された。
継承を行う際には、継承元となるクラスでは、そこから派生されるクラスについて何らかの想定をしてコードを書くことはできない。それができるのであれば、Aクラスのhelloメソッドに「super().hello()」呼び出しを追加することで、MROにおいてAの次にあるCのhelloメソッドが呼び出されるようできるだろう。
この場合、Aクラスを継承しない(が同名のhelloメソッドを持つ)Cクラスと、Aクラスを継承するBクラスという2つのクラスが定義されるのであれば、上記のような記述が可能だ。しかし、本当にそんなことがあるかは誰にも分からない。
よって、あるクラスを継承する際に、オーバーライドしたメソッドから基底クラスのメソッドを自分が想定したように連鎖させるにはあくまでも後からクラスを定義する側が調整する必要がある(もちろん、何らかのフレームワークのように、その利用者に対して「このクラスを利用するのであれば、利用する側ではこれこれこのようなメソッドを定義して、そこではこれこれこのような処理をしなければならない」というように、利用者に強制することは可能だが、ここではそこまでのことは想定していない。いずれにせよ、他者が作ったクラスを利用する際には、その作法に従いながら、自分のやり方を実践する必要があるということだ)。
もう一つ、super関数呼び出しに引数を渡すことでも、同じことを実現できる。以下に例を示す。
class D(B, C):
def hello(self):
print('Hello from D')
super(A, self).hello()
d = D()
print(D.__mro__)
d.hello()
こちらの方法で、上の確認コードを実行した結果を以下に示す。
詳しい説明は省略するが、「super(A, self).hello()」によりMROで「A」の次にあるクラス、つまりCが検索されて、そのインスタンスメソッドであるhelloがselfを使って呼び出される(興味のある方はPythonのドキュメント「デスクリプタの呼び出し」などを参照されたい)。この方法でも、MROで示された順序に従うことなく、Dクラスのhelloメソッドから、Cクラスのhelloメソッドを呼び出せる。ただし、super関数の動作とMROの値を理解した上でこのようなコードを書くよりも、ここでは素直に上で見たような「C.hello(self)」のような書き方をするのがよいかもしれない。
多重継承を行う際には、そのメソッド呼び出しがどのような順序で問題になる可能性があることと、PythonではMROを調べることで、メソッド呼び出しの解決順序が明確になることは覚えておこう。
今回はPythonの多重継承と、そのメソッド呼び出しがどのように解決されるかについて見た。次回は、多重継承におけるもう一つの注意点について見ていくことにする。
Copyright© Digital Advantage Corp. All Rights Reserved.