Pythonでのメソッドのオーバーライド
著者: Leonardo Giordani - 19/05/2014
はじめに
オーバーライド(Override) とは何ですか?オーバーライドとは、クラスの祖先が提供しているメソッドの実装を変更する機能のことです。
オーバーライドは、OOPにおいて非常に重要な要素であり、継承を最大限に生かすための機能です。メソッドのオーバーライドにより、クラスは他のクラスを「コピー」することができ、コードの重複を避けることができ、同時にクラスの一部を強化またはカスタマイズすることができます。このように、メソッドのオーバーライドは、継承のメカニズムの重要な部分を占めています。
継承の早見表
多くのOOP言語と同様に、Pythonの継承(Inheritance) は暗黙の委譲(Delegation) によって機能します。オブジェクトが要求を満たすことができない場合、多重継承の場合は言語固有のルールに従って、まず祖先に要求を転送しようとします。
例を挙げます。
code: python
class Parent(object):
def __init__(self):
self.value = 5
def get_value(self):
return self.value
class Child(Parent):
pass
見ての通り、Childクラスは空ですが、Parentを継承しているので、Pythonが全てのメソッドコールのルーティングを担当しています。そのため、Childオブジェクトのget_value()メソッドを使用しても、すべて期待通りに動作します。
code: python
>> c = Child()
>> c.get_value()
5
確かに get_value() は、Childクラスの中で定義されているかのように、正確にはChildクラスの一部ではありません。
code: python
>> p = Parent()
>> c = Child()
>>
>> dir(p)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'get_value', 'value']
>>
>> dir(c)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'get_value', 'value']
>>
>> dir(Parent)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'get_value']
>>
>> dir(Child)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__', '__module__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'get_value']
>>
>> Parent.__dict__
dict_proxy({'__module__': '__main__',
'get_value': <function get_value at 0xb69a656c>,
'__dict__': <attribute '__dict__' of 'Parent' objects>,
'__weakref__': <attribute '__weakref__' of 'Parent' objects>,
'__doc__': None,
'__init__': <function __init__ at 0xb69a6534>})
>>
>> Child.__dict__
dict_proxy({'__module__': '__main__', '__doc__': None})
これは、Childクラスには実際には get_value() メソッドが含まれておらず、自動委譲のメカニズムがフードの下で作動していることを示しています。このメカニズムについては、こちらの記事をご覧ください。
メソッドのオーバーライドをしてみる
Pythonのメソッドオーバーライドは、親クラスのメソッドと同じ名前のメソッドを子クラスで定義するだけで起こります。オブジェクトにメソッドを定義すると、後者がそのメソッド呼び出しを満たすことができるようになるので、その祖先の実装は関係ありません。
code: python
class Parent(object):
def __init__(self):
self.value = 5
def get_value(self):
return self.value
class Child(Parent):
def get_value(self):
return self.value + 1
これで、Childオブジェクトの動作が変わります。
code: python
>> c = Child()
>> c.get_value()
6
クラスの内部を見てみると、以下のような違いがあります。
code: python
>> Parent.__dict__
dict_proxy({'__module__': '__main__',
'get_value': <function get_value at 0xb69a656c>,
'__dict__': <attribute '__dict__' of 'Parent' objects>,
'__weakref__': <attribute '__weakref__' of 'Parent' objects>,
'__doc__': None,
'__init__': <function __init__ at 0xb69a6534>})
>>
>> Child.__dict__
dict_proxy({'__module__': '__main__',
'get_value': <function get_value at 0xb69a65a4>,
'__doc__': None})
というのは、Childクラスには、実際には異なる実装の get_value() メソッドが含まれているからです(2つの関数のidは異なります)。
これはPythonでは非常に重要なことです。継承の委譲は自動的に行われますが、あるメソッドがオーバーライドされた場合、先祖の実装は全く考慮されません。そのため、自分のクラスの先祖の実装を実行したい場合は、明示的にそれらを呼び出さなければなりません。
なぜ、クラス階層のより深いところにあるオブジェクトの実装を呼び出したいのでしょうか?
それは、多くの場合、メソッドをオーバーライドしてその性質を強化する、つまり結果の「質」を向上させるために呼び出すことがあり、何かを向上させるためにはまずそれにアクセスする必要があるからです。つまり、元の実装を呼び出すことで、後から改善したい結果を得ることができるのです。
しかし、常にオリジナルの実装を呼び出さなければならない明確な理由があります。この理由は「隠れた副作用」と呼ばれるものです。
クラスを継承するということは、実際には内部構造がわからない(と思われる)クラス階層全体を継承することになります。これは、どのメソッド呼び出しも、クラス階層全体に対する複雑な操作のセットを隠すかもしれないことを意味し、そのうちのいくつかは、あなたが使用しているライブラリやフレームワークにとって重要かもしれません。
Pythonはオーバーライドされたメソッドの元の実装を明示的に呼び出すようにしています(他のオブジェクト指向言語と変わりません)。これは確かに「Explicit is better than implicit(暗示するより明示するほうがいい)」(The Zen of Python)というPythonの考えに沿ったものですが、このアドバイスは単なる好みの問題や、ある種のプログラミングのマナーではありません。
オーバーライドする際には、元の実装の引数をフィルタリングしたいのか、その結果をフィルタリングしたいのか、あるいはその両方なのかを考えなければなりません。一般的には、親の実装が処理するデータを変更したい場合は、引数をフィルタリング(プレフィルタ(Pre-Filter))し、追加の処理層を追加したい場合は、結果をフィルタリング(ポストフィルタ(Post-Filter))したいと考えます。もちろん、どちらも同じメソッドで一緒に行うことができます。親の実装を明示的に呼び出さなければならないので、新しいメソッドのコードのどこでそれを行うかは自由です。
プレフィルタリングの例
code: python
import datetime
class Logger(object):
def log(self, message):
print message
class TimestampLogger(Logger):
def log(self, message):
message = "{ts} {msg}".format(ts=datetime.datetime.now().isoformat(),
msg=message)
super(TimestampLogger, self).log(message)
TimestampLogger オブジェクトは、オリジナルの実装である log() メソッドを呼び出す前に、メッセージ文字列にいくつかの情報を追加します。
code: python
>> l = Logger()
>> l.log('hi!')
hi!
>>
>> t = TimestampLogger()
>> t.log('hi!')
2014-05-19T13:18:53.402123 hi!
>>
ポストフィルタリングの例
code: python
import os
class FileCat(object):
def cat(self, filepath):
f = file(filepath)
lines = f.readlines()
f.close()
return lines
class FileCatNoEmpty(FileCat):
def cat(self, filepath):
lines = super(FileCatNoEmpty, self).cat(filepath)
return nonempty_lines
FileCatNoEmpty オブジェクトを使用すると、空の行が取り除かれたFileCatオブジェクトの結果が得られます。
ご覧のように、最初の例ではオリジナルの実装が最後に呼び出されていますが、2番目の例では他のすべてのものの前に呼び出されています。したがって、オリジナルのメソッドを呼び出すための固定された位置というものはなく、何をしたいかによって異なります。
常にsuper()を呼び出すべきか?
元のメソッドの実装を常に呼び出すようにしましょうか。理論的には、よく設計されたAPIは常にそれを可能にするはずですが、境界ケースが存在することを知っています:オリジナルのメソッドは避けたい副作用があるかもしれませんし、APIがそれらを避けるためにリファクタリングできないこともあります。そのような場合には、元のメソッドの実装への呼び出しをスキップすることを好むかもしれません。Pythonはそれを義務化していませんので、状況がそれを必要とすると思う場合には、自由にその道を歩むことができます。しかし、自分が何をしているのかを確実に把握し、なぜメソッドを完全に上書きしているのかを文書化してください。
まとめ
可能な限り、オーバーライドしているメソッドのオリジナルの実装を呼び出すようにしましょう。これにより、基礎となるAPIが期待通りに動作するようになります。呼び出しをスキップする必要がある場合は、その理由を必ず文書化してください。
メソッドのオリジナルの実装を呼び出すために、Python 2.xでは super(cls, self)、Python 3.xではsuper() を常に使用してください。これは多重継承の場合の解決順序を尊重し、Python 3.xではクラス階層の変更から保護します。
メソッドのオリジナルの実装を呼び出す場合は、メソッドを実行するために必要なすべてのデータが揃った時点で実行してください。
参考