Pythonのデコレータについて

  • 172
    いいね
  • 0
    コメント

今回は、Pythonデコレータについて話そうと思います。

はじめに。

expart python programingに沿って勉強していた際に、Pythonのデコレーターという概念が出てきました。ちょっと本に書いてある内容では何を言っているかわからなかったので、いろいろ調べてみました。

参考URL

下記にURLを参考にさせていただいたので、掲載させていただきます。

デコレーターとは

デコレートとは修飾する。って意味になりますが、デコレータとは、簡単に言うと、ある関数を修飾するための関数とその仕組みです。例えば、ある関数があったとします。ここでは次のようなtest()関数とその実行スクリプトを見てみましょう。

サンプルスクリプト1

test.py
def test():
    print 'Hello Decorator'

test()

実行結果は次のようになります.

Hello Decorator

まあ当たり前ですよね。

このtest関数をデコレートしていきます。

デコレーターの簡単な例

上記のスクリプトをデコレーターを使ってデコレートしたものが次のようになります。

sample_deco1.py
def deco(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        print '--start--'
        func(*args,**kwargs)
        print '--end--'
    return wrapper

@deco
def test():
    print 'Hello Decorator'

test()

実行結果が次のようになります。

--start--
Hello Decorator
--end--

さて、これがどうなっているかを説明します。

なかみの簡単な説明

1行目def deco(func):から8行目return wrapperがデコレートするためのデコレーターの定義関数です。この関数に従ってデコレートします。10行目@decoは、「deco」というデコレーターの定義関数で次の関数をデコレートしますよ。ってことになります。

では、デコレーターの定義関数の中身について話していきます。
重要なのは5行目print 'start'から7行目print 'end'です。

1行目def deco(func):はデコレーター関数の宣言です。引数にfuncと指定していますが、ここには、デコレートされる関数であるtest()自体の参照先が引数として渡されています。

2行目import functoolsと3行目@functools.wraps(func)は、デコレータのおまじないです。説明がめんどくさいので、おまじないと言われて嫌な人は、上記の参考URLを確認してください。

4行目def wrapper(*args,**kwargs):は、実際のデコレーターの中身を書くための関数です。よくわからなかったらこういうもんだと把握しながらとりあえず進みましょう。詳しくは参考URLを。

さて、重要な5行目から7行目です。
6行目func(*args,**kwargs)では、引数として渡された関数が実行されます。
5行目print 'start'と7行目print 'end'では、6行目func()の前と、後に実行される処理を示します。

で、*args,**kwargsは、デコレートする関数の引数です。まあどんな関数でも何も考えずにこういう風に書いておいたら大丈夫です(多分)

つまりデコレータで何ができるのか

デコレーターを利用することで、既存関数の処理の前後に自分自身で、処理を付け加えることができます。どういう状況でそういうことをするかということを考えてみました。僕自身もまだ勉強したてでわかっていない部分が多いのですが、デコレーターを使うことで、ライブラリとして提供されている関数を呼んだときに自動的に実行される処理を付加できるということになります。

Pythonでは、Javaなどで利用されるオーバーライドのような形で関数自体を書き換えてしまうことも可能ではあります。しかしながら、既存の処理には問題がないんだけど、それだけじゃなくて、違う処理も同時にさせたい。みたいな状況のときに、デコレータを使うんだと思います。

参考URLでは、デコレーターを利用して、ある関数のベンチマークを行うプログラムが紹介されていました。

なるほど、ベンチマーク用のデコレーター関数を一つ作ってしまえば、あとはいろんな関数に@でデコレートしていくだけで、それぞれのベンチマークを計測できたりしますよね。便利ですね。

返り値を持つメソッドでのデコレーターの例

上記の例では、デコレートされるメソッドは、返り値を持ちませんでした。しかし、メソッドは当然ながら、返り値を持つ時もあります。ここでは、返り値を持つメソッドでのデコレータの例を記載します。

sample_deco2.py
def deco2(func):
    import functools
    import os
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        res = '--start--' + os.linesep
        res += func(*args,**kwargs) + '!' + os.linesep
        res += '--end--'
        return res
    return wrapper

@deco2
def test2():
    return 'Hello Decorator'

print test2()

このスクリプトの実行結果が以下になります。

--start--
Hello Decorator!
--end--

ああ!?実行結果、変わってねえじゃねえか!!って?いや、微妙に変わってます。出力の一部がHello DecoratorからHello Decorator!に変わりました。プログラムの中身も結構変わっています。このプログラムをさっきのプログラムと比較しながら説明して行きます。

まず、さきほどのプログラムと比較して、デコレートされるメソッドdef test2():は、文字列の返り値return 'Hello Decorator'を持つようになりました。そして、print test2()が、戻ってきた文字列を出力しています。

さっきまでは、デコレートされるメソッドは、文字列を出力するメソッドでしたが、今回デコレートされるメソッドは、文字列を生成するメソッドであるということです。何かを生成するメソッドをデコレートする場合、生成された結果が返ってくるようにデコレータを書かなければなりません。

デコレータの中身で先ほどと変わった場所をピックアップします。
6行目のres = func(*args,**kwargs) + '!'
8行目のreturn res
9行目のreturn wrapper

6行目では、func(**kwargs)を実行して返ってきた文字列の末尾に'!'を付け足してresに格納しています。
8行目ではresを返します。それを9行目のreturn wrapperでさらにデコレートされるメソッドに返しています。

つまりどういうことか

デコレータは、値を返すメソッドをデコレート可能なだけではなく、デコレートするメソッドの返り値を加工することが可能であるということです。これは非常に面白いことです。デコレータするのプログラムの出力結果にある特定の処理を加えることが可能と考えれば・・・?いろいろ楽しくなってきますね。

さらに言えば、例えば、htmlやxmlのようにタグで囲まれる記法はデコレータを利用すれば中身だけ生成するメソッドで記載できることになります。

あれ?例えば、htmlをデコレータで書くなら、ネストさせたいな・・・。はい。デコレータはネストすることもできます。

デコレータをネストさせる

複数のデコレータを組み合わせることも可能です。
例えば、以下のような状態です。

deco_sample3.py
def deco_html(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        res = '<html>'
        res = res + func(*args,**kwargs)
        res = res + '</html>'
        return res
    return wrapper

def deco_body(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        res = '<body>'
        res = res + func(*args,**kwargs)
        res = res + '</body>'
        return res
    return wrapper

@deco_html
@deco_body
def test():
    return 'Hello Decorator'

print test()

実行結果は以下のようになります。

<html><body>Hello Decorator!</body></html>

このプログラムは、これまでのプログラムがわかれば、何も難しくありません。def test(str):をデコレートするデコレータが二つになっただけです。下にあるものから順番に処理されて行きます。

なかなか楽しくなってきましたね。デコレータって色んなことができそうだ。あれ?そういえば、引数をもつメソッドにデコレータを使うことはできないんだろうか・・・。できます。

引数をもつメソッドをデコレートする

引数を持つメソッドのデコレートは以下のように作成します。

deco_sample4.py
def deco_p(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args,**kwargs):
        res = '<p>'
        res = res + func(args[0],**kwargs)
        res = res + '</p>'
        return res
    return wrapper

@deco_p
def test(str):
    return str

print test('Hello Decorator!')

実行結果は以下のようになります。

<p>Hello Decorator!</p>

さて、先ほどまでのプログラムと変わった部分はどこでしょう。
func(args[0],**kwargs)です。実はデコレータはデコレートするメソッドが引数を持っているかどうかに関係なく、引数を受け取れる状態になっています。引数を利用する場合は、argsから引数を取り出して、funcに渡して上げます。そして、面白いのは、argsがタプルであるという点です。

タプルということはどういうことでしょうか。とりあえず少なくともビルトインメソッドのlen()は利用できますね。ということは、引数の数によって、デコレータの挙動を変えたりできるということです。ていうか、そもそもデコレータに引数って渡せないのかな・・・。渡せます。

デコレータに引数を渡す

deco_sample5.py
def deco_tag(tag):
    def _deco_tag(func):
        import functools
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            res = '<'+tag+'>'
            res = res + func(*args,**kwargs)
            res = res + '</'+tag+'>'
            return res
        return wrapper
    return _deco_tag

@deco_tag('html')
@deco_tag('body')
def test():
    return 'Hello Decorator!'

print test()

出力結果は以下のようになります。

<html><body>Hello Decorator!</body></html>

疲れてきた説明を省きますが、ここではデコレータをネストしています。

おわり

今回は、デコレータについて色々試して、その内容をここに書いてみました。デコレータって面白いですね。いろんなことができそうです。例えば、これをジェネレータと組み合わせてみたら・・・、本当に色んなことができそうです。一番最初に書いた参考URLにはもっと詳しいデコレータの説明がありますので、興味がわけばどうぞ。

ではでは。