インスタンスメソッド用デコレータを書いてみた

そう言えば関数のデコレータってそこそこ書いたことあるけどインスタンスメソッド用のデコレータって作ったことないなと思い挑戦。
通常の関数のデコレータとは微妙に違うようなのでその辺も踏まえて比較もしてみた(最後はどっちでも使えるようにしたけど。

デコレータはいつもどうやって書いたっけ?となるのでメモの意味も兼ねてここに記載。

インスタンスメソッドと関数の違い

Pythonでのインスタンスメソッドは必ず第1引数に自分自身のオブジェクトを表すselfという変数が入る。
下記のコードを実行するとbar()の呼び出したところでメソッドの引数の個数が合わないというエラーを返す。

class Hoge(object):

    def foo(self):
        pass

    def bar():
        pass

hoge = Hoge()
hoge.foo()
hoge.bar()

エラーメッセージは以下。

Traceback (most recent call last):
  File "hoge.py", line 14, in <module>
    hoge.bar()
TypeError: bar() takes no arguments (1 given)

通常の関数へのデコレータ適用

まずただの関数へのデコレータの書き方。

def deco(func):
    def _deco():
        print "====================="
        result = func()
        print "====================="
        return result
    return _deco

def hoge():
    print "hogehoge"

@deco
def deco_hoge():
    print "hogehoge"

print "call hoge()"
hoge()
print ""
print "call deco_hoge()"
deco_hoge()

デコレータがどういう風に解釈されているかというのは別のサイトの参照してもらうとして*1、deco()関数の中の_deco()関数の引数は何もなし。

実行結果は以下。

call hoge()
hogehoge

call deco_hoge()
=====================
hogehoge
=====================

インスタンスメソッドへのデコレータ適用

適当なクラスを作って先ほどのdecoデコレータを単純に適用してみたクラスを作った。

class Piyo(object):
    def piyo(self):
        print "piyopiyo"
    @deco
    def deco_piyo(self):
        print "piyopiyo"

piyo = Piyo()
print "call piyo.piyo()"
piyo.piyo()
print ""
print "call piyo.deco_piyo()"
piyo.deco_piyo()

これを実行すると以下のようなエラーになる。

call piyo.piyo()
piyopiyo

call piyo.deco_piyo()
Traceback (most recent call last):
  File "deco.py", line 38, in <module>
    piyo.deco_piyo()
TypeError: _deco() takes no arguments (1 given)

_deco()関数に何やら1つの引数が与えられているらしい。
これはdeco_piyo()メソッドの引数selfだろうと思われるのでとりあえず動くように_deco()関数の引数にselfを追加。(hogeの動作確認の部分はエラーが出るのでとりあえずコメントアウトしておく。詳細は後述。)

def deco(func):
    def _deco(self):
        print "====================="
        result = func(self)
        print "====================="
        return result
    return _deco

これで改めて実行すると以下のように出力される。

call piyo.piyo()
piyopiyo

call piyo.deco_piyo()
=====================
piyopiyo
=====================

通常の関数でもインスタンスメソッドでも動くようにする

インスタンスメソッドで動くように変えると通常の関数に適用されているdecoデコレータの引数の個数が合わなくなるので手っ取り早くリストによる可変引数を適用。
可変引数にしたdecoデコレータは以下。

def deco(func):
    def _deco(*args):
        print "====================="
        result = func(*args)
        print "====================="
        return result
    return _deco

これで実行するとどちらでも動くようになり、以下の出力がされる。

call hoge()
hogehoge

call deco_hoge()
=====================
hogehoge
=====================

call piyo.piyo()
piyopiyo

call piyo.deco_piyo()
=====================
piyopiyo
=====================

最後に

実際はfunctools.wrapsとか使ってやるのが正しいっぽいので、こちらはまた次回にでもやってみる。
この辺はたしかエキPyことエキスパートPythonプログラミングの初めの方にに詳しく書かれているはずなので気になる人は読んでください。

エキスパートPythonプログラミング

エキスパートPythonプログラミング