Python のデコレータの使い方

Python のデコレータの使い方について見てみます。

デコレータとは、関数やクラスを「装飾」する機能のこと。デコレータを使うことで、既存の関数やクラスの中身を直接触ることなく、その外側から機能を追加したり書き換えたりすることができます。

Python では、関数やクラスもオブジェクトということもあって、自分オリジナルのデコレータをかんたんに作ることができます。デコレータは @ 文字を使用して記述します。

具体的に見ていきましょう。次のような hello 関数が定義されているものとします。

# あいさつを返す関数
def hello():
    return "konichiwa"

print hello()  # => konichiwa
# 定義したとおりの結果が返ってくる

続いて、 mydec 関数を定義し hello 関数を書き換えます。

# デコレータパターンを使うために
# 関数を受け取り関数を返す関数を定義する
def mydec(func):
    def new_func():
        print "%s function called".format(func.__name__)
        return func()
    return new_func

# hello を書き換え
hello = mydec(hello)

print hello()
# => hello function called
#    konichiwa

書き換えた結果、新たに作られた hello 関数は元の hello 関数と同じ戻り値を保ちながら、標準出力に hello function called と出力するものに変わりました。

すでにある関数を加工するこの一連の処理がデコレータパターンです。キモは hello = mydec(hello) の一文です。

Python ではこの一文は @ を使ったシンタックスシュガーでシンプルな形に書き換えることができます。これがいわゆるデコレータ構文です。

@mydec
def hello():
    return "konichiwa"

このコードは次のコードと等価です。

def hello():
    return "konichiwa"
hello = mydec(hello)

@デコレータ名 」をいくつも重ねて、複数のデコレーションを行うことも可能です。その際、適用順序は後に書かれたデコレータが先になることに注意する必要があります。

@mydec1
@mydec2
def hello():
    return "konichiwa"
# この場合、 hello = mydec1(mydec2(hello)) と書いたのと同じ結果になる

上記の hello は引数を持たない関数でしたが、引数がある関数に適用できるデコレータも、可変長引数などを使ってかんたんに定義することができます。

def mydec(func):
    def new_func(*args, **kwargs):
        print%s function called”.format(func.__name__)
        return func(*args, **kwargs)
    return new_func

@mydec
def some_function(*args):
    # ... content

ただし、このやり方では、関数のアトリビュート __name____doc__ が書き換えられてしまうという問題があります。

この書き換え問題を手軽に解決できるのが、標準ライブラリの functools.wraps デコレータです。これを使えば、書き換え前の関数のアトリビュートを書き換え後のものに手軽に移すことができます。

from functools import wraps

def memoize(func):
    cache = {}
    # wraps を使って元の関数のアトリビュート等を書き換え後の関数にコピーする
    @wraps(func)
    def decorated_func(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return decorated_func

@memoize
def some_function(*args):
    # ... content

一見ややこしいのですが、 wraps は次のような形で使うデコレータです。

@wraps(オリジナルの関数)
def 書き換え後の関数():
    # 書き換え後の関数の中身

オリジナルの関数のアトリビュートを書き換え後の関数にコピーする必要があるのでよくよく考えればこの書き方になるのも納得ですが、一見コードを見ても何をしているのかわかりません。このあたりは定型パターンということで、難しく考えずに「こう書くもの」で流してしまってもよいかもしれません。

以上です。

ちなみに、 @ 構文は関数だけでなくクラスに対しても適用することができます。また、デレコータそのものも関数ではなくクラスとして定義することなんかも可能です。

もっと詳しく見てみたい場合は、わかりやすい解説をしている方がたくさんいらっしゃるので参考ページを参照してみてください。

参考

いろんなデコレータパターンを紹介してあります:

日本語で書かれている解説ではこのあたりのページがおすすめです:

公式ドキュメントの解説。ちょっと短すぎるような…