[Python入門]多重継承Python入門(1/2 ページ)

Pythonでは複数のクラスを基にクラスを定義する「多重継承」が可能だ。その方法と、多重継承時にメソッドが呼び出される仕組みについて見ていこう。

» 2019年08月27日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「Python入門」のインデックス

連載目次

 前回はクラスの(インスタンス)メソッドのスコープとselfの役割、それからプライベートな属性について見た。今回はクラスの多重継承と、多重継承をしたときにメソッドがどのように解決されるかについて見てみよう。

多重継承とは

 「多重継承」とは「複数のクラスからその機能を継承する」ことだ。例えば、「AクラスとBクラスを基にCクラスを作成する」のがそうだ。「AクラスからBクラスを派生させ、BクラスからCクラスを派生させる」のは継承(単一継承)の連鎖であって多重継承ではないことには注意しよう。

 さまざまな理由から最近のプログラミング言語では多重継承をサポートするものはそれほど多くはないが、Pythonは多重継承をサポートしている。多重継承を行うには、クラス定義で基底クラスを複数記述するだけだ。

 以下に例を示す。

class A:
    def hello(self):
        print('Hello from A')

class B(A):
    pass

class C(A):
    def hello(self):
        print('Hello from C')

class D(B, C):
    pass

多重継承の例

 この例では、Aクラスを大本のクラスとして定義している(実際には、その親としてobjectクラスがある)。そして、その直接の派生クラスとしてBクラスとCクラスを定義して、今度はそれら2つのクラスを基にDクラスを定義している。なお、AクラスとCクラスではhelloメソッドを定義しているが、他の2つのクラスでは定義していない。

 このDクラスの定義では、「class D(B, C)」として基底クラスをカンマ区切りで複数指定している。これにより多重継承が行われ、DクラスはBクラスとCクラスを直接の基底クラスとするようになる。

どのクラスのメソッドが呼び出されるか

 上のようにして、定義されたDクラスを使用する際には注意しておくべきことがある。つまり、「Dクラスのインスタンスに対してhelloメソッドを呼び出すと、AクラスとCクラスのどちらのhelloメソッドが呼び出されるか」だ。

 メソッドを呼び出す際に「どのメソッドが呼び出されるかを特定する」ことを「メソッドの解決」(method resolution)と呼ぶことがある。そして、この場合、メソッドを解決するには「D→B→A(→C→A)」という順序でhelloメソッドを探して最終的に「Aクラスのhelloメソッドに解決される」経路と、「D→B→C(→A)」という順序でhelloメソッドを探して最終的に「Cクラスのhelloメソッドに解決される」経路の2つの経路がありそうなことは分かるだろう*1。このように、メソッドを解決していく順序のことを「MRO」(Method Resolution Order:メソッド解決順序)と呼ぶ。なお、上の経路でかっこに囲まれているところは、その直前でhelloメソッドが見つかるので実際には検索が行われないことを示す。

*1 前者は「深さ優先」と呼ばれるアルゴリズムで、「まずDの直接の基底クラスであるB、次のその直接の基底クラスであるA」の順に検索して、そこでメソッドが見つからなければ「もう一つの直接の基底クラスであるC、次にその直接の基底クラスであるA」と検索をしていくものだ。後者は「幅優先」と呼ばれるアルゴリズムで、「まずDの直接の基底クラスであるBとC」を検索して、そこでメソッドが見つからなければ「それらの共通の基底クラスであるA」を検索するというものだ。ただし、Pythonではこれらとは異なるアルゴリズムを用いてメソッド検索が行われている。


 では、実際にどちらのメソッドが実行されるかを試してみよう。

d = D()
d.hello()

どちらのhelloメソッドが呼び出されるかを確認するコード

 これを実行すると、次のようになる。

実行結果 実行結果

 ご覧の通り、Cクラスのメソッドが呼び出された。この場合には「D→B→C→A」という順序でメソッドが検索されているのだが、その順序はPython 3(およびPython 2.3以降)では「C3線形化」と呼ばれるアルゴリズムを用いて決定され、メソッド検索の中であるクラス(この場合はAクラス)が何度も登場することがないようになっている。

 ここではA〜Dのクラスの継承階層が次のようになっている。

A〜Dのクラスの継承階層 A〜Dのクラスの継承階層

 このように菱形(ダイヤモンド形)に継承階層が構成される際に、どのメソッドが呼び出されるかや同じクラス(この場合はAクラス)がメソッド解決時に何度も登場することが問題になる。これを「ダイヤモンド継承問題」とか「菱形継承問題」などと呼ぶことがある。

 また、先ほども述べたが、Pythonでは「深さ優先」でも「幅優先」でもない「C3線形化」と呼ばれるアルゴリズムでどのメソッドが呼び出されるかが決定される。先ほどのコードを少し変更して、これを確認してみよう。

class A:
    def hello(self):
        print('Hello from A')

class B(A):
    pass

class C:
    def hello(self):
        print('Hello from C')

class D(B, C):
    pass

CクラスはAクラスを継承していないことに注意

 今度はCクラスはAクラスの派生クラスではなく、objectクラスを継承している。継承階層を示すと次のようになる。

クラスの継承階層 クラスの継承階層

 これでDクラスのインスタンスを作成して、helloメソッドを呼び出してみよう。

d = D()
d.hello()

どちらのhelloメソッドが呼び出されるかを確認するコード

 すると、実行結果は次のようになる。

実行結果 実行結果

 今度はAクラスのhelloメソッドが呼び出された。これは「D→B→A→C」の順序でメソッドが検索されて(「A」でhelloメソッドが見つかって)いるからだ。最初の例では「幅優先」(直接の基底クラスを優先)したように見えて、今度は「深さ優先」(最初に検索をしたクラスの継承階層を優先)しているように見える。このことから、PythonにおけるC3線形化によるメソッド検索がどちらでもないことが分かるはずだ。

 今度は上のコードでDクラスの定義でBとCの順序を変えてみよう。

class A:
    def hello(self):
        print('Hello from A')

class B(A):
    pass

class C:
    def hello(self):
        print('Hello from C')

class D(C, B):
    pass

DクラスはCクラスとBクラスを継承している

 これで先ほどと同じ以下のコードを実行してみよう。

d = D()
d.hello()

どちらのhelloメソッドが呼び出されるかを確認するコード

 すると、実行結果は次のようになる。

実行結果 実行結果

 今度は(大方の予想通り)Cクラスのhelloメソッドが呼び出された。

MRO:メソッド解決順序

 このようにクラス定義時の基底クラスの指定が違うだけでも、どのメソッドが呼び出されるかは変わってくる。C3線形化のアルゴリズムを正確に理解して、その順序を自分で導き出さなければ、多重継承は難しくて使えない。と思うかもしれないが、そうしたことは、もちろんPythonが面倒を見てくれる。実は、クラスには「__mro__」という特殊属性があり、これを参照することでメソッドを解決する際に基底クラスをたどっていく順序が分かるようになっている。「mro」は上で述べたように「Method Resolution Order」の略だ。なお、MROは多重継承に限ったものではなく、単一継承の継承階層でもそれに応じたものが作成される。

 実際に試してみよう。

print(D.__mro__)

DクラスのMROを表示する

 これを実行すると、次のようになる。

実行結果 実行結果

 この結果からはメソッドが「D→C→B→A→object」という順序で検索されることが分かる。objectクラスは全てのクラスの基底クラスなので、最後の最後に検索の対象となる。

 このように、objectクラスが全ての基底クラスになっているので、実は上で示したようなAクラスがない場合でもクラスの継承階層はダイヤモンド状になる。このため、上で見たのと同じような問題がPython 3では常に発生する。

 以下はobjectクラスをダイヤモンドの頂点として、それを継承するBとCの2つのクラス、さらにそれら2つのクラスを継承するDクラスを定義したものだ。ただし、「class D(C, B)」と基底クラスのリストにはCクラスが先に書かれていることに注意しよう。

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):
    pass

DクラスはCクラスとBクラスを継承する

 ここでDクラスのインスタンスを生成するとどうなるだろう。

d = D()
print(D.__mro__)

Dクラスのインスタンス生成時にどのクラスの__init__メソッドが呼び出されるかを確認するコード

 実行結果を以下に示す。

実行結果 実行結果

 この場合は、Cクラスの__init__メソッドが呼び出される(「class D(C, B)」となっているので、MROではCが先に検索されるため)。一方、この出力からはBクラスの__init__メソッドが呼び出されていないことも分かる。つまり、DクラスはCクラスとBクラスを継承しているが、Bクラスの__init__メソッドで定義しているはずのインスタンス変数b_valueが定義されていないということだ。

print(d.b_value)

インスタンスdでインスタンス変数b_valueが定義されているか

 実際に上のコードを試せば、そのことが分かる。

実行結果 実行結果

 この通り、あるべきはずのインスタンス変数が定義されていないことが分かった。適切に初期化を行うには、__init__メソッドが全て呼び出される必要がある。そして、それにはもちろんsuper()関数を使って、継承階層に含まれる全ての__init__メソッドが呼び出されるようにする必要があるということだ。

       1|2 次のページへ

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。