Pythonのクラス、最速理解特集:Visual Studioで始めるPythonプログラミング(4/4 ページ)

» 2016年11月11日 05時00分 公開
[かわさきしんじInsider.NET編集部]
前のページへ 1|2|3|4       

多重継承

 多重継承を行う場合には、クラス定義時に基底クラスとなるクラスを列挙していく。以下に例を示す(クラス名やメソッド名は上の例よりもシンプルにした)。

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


D3クラスはD1クラスとD2クラスを継承する

 これは典型的なダイヤモンド継承だ。

ダイヤモンド継承 ダイヤモンド継承

 ダイヤモンド継承においては、メソッド呼び出しをどう解決するかが問題になる(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クラスのインスタンスメソッドmが呼び出された

 ここでは、D1クラスのメソッドが呼び出された。次にもう一度、実験をしてみる。D3クラスの定義での基底クラス指定を「(D2, D1)」にした以外は同じものを実行した結果を以下に示す。

…… 省略 ……
>>> class D3(D2, D1):
...   pass
... 
>>> d3 = D3()
>>> d3.m()
m on D2


今度は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'>]


最初のD3クラスではD3→D1→D2→B→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'>]


変更後のD3クラスではD3→D2→D1→B→objectの順に検索が行われる

 また、組み込み関数superを使うと多重継承時のメソッド探索をカスタマイズできる(単一継承時には上でも見たようにオーバーロードされたメソッドから基底クラスのメソッドを呼び出すのに使える。ただし、単一継承時でもMROによって検索が行われることには変わりはない。多重継承ではMROの決定が単一継承よりも複雑になるということだ)。例えば、上のD3クラスは次のように変更できる。

…… 省略 ……
class D3(D1, D2):  # 基底クラスの指定はD1、D2の順序
  def m(self):
    super(D3, self).m()

d3 = D3()
d3.m()


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


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

 今度はD3クラスを次のように変更してみる。

class D3(D1, D2):
  def m(self):
    super(D1, self).m()

d3 = D3()
d3.m()


D1の「親または兄弟」となるクラスにインスタンスメソッドmの呼び出しを委譲する

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

>>> d3 = D3()
>>> d3.m()
m on D2


今度はD2クラスのメソッドが呼び出される

 ソースコードまで追えなかったので推測になるが、これにより、D3.__mro__属性の値でD1よりも後ろにあるもの(つまり、親または兄弟となるクラス)に対してメソッド解決が行われるようになると思われる。

 多重継承を行った場合のメソッド呼び出しの解決はプログラマーの頭を悩ませるものだが、Pythonでは明確なルール(MRO)とそのカスタマイズ手段が与えられている。とはいえ、シンプルさを重要視するのであれば、本稿では触れられなかったがミックスインなどの手法を採用するのがよいかもしれない。


 本稿ではPythonのクラスについてざっくりと一巡りしてきた。次回はモジュールについて見ていく予定だ。

「特集:Visual Studioで始めるPythonプログラミング」のインデックス

特集:Visual Studioで始めるPythonプログラミング

前のページへ 1|2|3|4       

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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