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

» 2019年08月27日 05時00分 公開
[かわさきしんじDeep Insider編集部]
前のページへ 1|2       

super関数とMRO

 そこで、まずは先ほどのコードを次のように修正しよう。

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__()

「super().__init__()」は何を呼び出すか

 これはobjectクラスをダイヤモンドの頂点としてBクラスとCクラスがそれを継承して、DクラスはCクラスとBクラスを継承するというものだ。そして、Dクラスの__init__メソッドは「基底クラスの__init__メソッド」を呼び出すようにしている。

 では、以下のコードでDクラスの動作を見てみよう。

d = D()
print(D.__mro__)

Dクラスの動作を確認するコード

 このコードを実行すると、次のような結果になる。

実行結果 実行結果

 上の出力の通り、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__()

「super().__init__()」は何を呼び出すか

 この状態で、以下のコードを実行してみよう。

d = 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()

「super().hello()」は何を呼び出すか

 この場合、MROが「D→B→A→C(→object)」となるのは既にお分かりだろう。このときに、以下のコードを試すとどうなるだろう。

d = D()
print(D.__mro__)
d.hello()

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のhelloメソッドからはCクラスのhelloメソッドを呼び出したい

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

d = D()
print(D.__mro__)
d.hello()

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関数に引数を渡して、MROで「A」の後にある「C」のhelloメソッドを呼び出す

 こちらの方法で、上の確認コードを実行した結果を以下に示す。

実行結果 実行結果

 詳しい説明は省略するが、「super(A, self).hello()」によりMROで「A」の次にあるクラス、つまりCが検索されて、そのインスタンスメソッドであるhelloがselfを使って呼び出される(興味のある方はPythonのドキュメント「デスクリプタの呼び出し」などを参照されたい)。この方法でも、MROで示された順序に従うことなく、Dクラスのhelloメソッドから、Cクラスのhelloメソッドを呼び出せる。ただし、super関数の動作とMROの値を理解した上でこのようなコードを書くよりも、ここでは素直に上で見たような「C.hello(self)」のような書き方をするのがよいかもしれない。

 多重継承を行う際には、そのメソッド呼び出しがどのような順序で問題になる可能性があることと、PythonではMROを調べることで、メソッド呼び出しの解決順序が明確になることは覚えておこう。

まとめ

 今回はPythonの多重継承と、そのメソッド呼び出しがどのように解決されるかについて見た。次回は、多重継承におけるもう一つの注意点について見ていくことにする。

今回のまとめ:

  • 多重継承を行うにはクラス定義で「class クラス名(基底クラス1, 基底クラス2, ……)」のように基底クラスをカンマ区切りで並べて指定する
  • 多重継承を行った際に、メソッドがどのように解決されるかはMRO(メソッド解決順序)で決定される
  • 多重継承を行ったクラスで適切にインスタンスの初期化を行うには、super関数を介して__init__メソッドを連鎖させる必要がある
  • MROで決定される順序に従わないで、基底クラスの特定のメソッドを呼び出すことも可能

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

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

メールマガジン登録

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