Python Tips: デコレータに引数を渡したい

Python のデコレータに引数を渡す方法について見てみます。

具体的には「引数を取ることができるデコレータの作り方」を見ていきます。

まずはかんたんにおさらいから。 Python では @デコレータ という形でデコレータを使うことができます。

# デコレータ関数 add_print() の定義
def add_print(func):
    def wrapper(*args):
        print("{} is invoked.".format(func.__name__))
        func(*args)

    # ラップした関数そのものを返す
    return wrapper

# デコレータ関数の利用
@add_print
def myfunc():
    print("Hello!")

# 動作確認
myfunc()
# =>
# myfunc is invoked.
# Hello!

myfunc() を add_print() でデコレートできていることが確認できます。

デコレータ機能をシンプルに使うだけの場合はこれでよいのですが、デコレータを使ってちょっと凝ったことをやろうとするとデコレータにパラメータを渡したくなってきます。今回はその方法を見ていきます。

作り方の前に使い方を先に見てみましょう。引数を受け取れるデコレータは次のように使います。

@デコレータ関数を生成する関数(引数)
def デコレート対象の関数(引数):
    関数の中身

引数を取らないデコレータの場合と見比べてみましょう。引数を取らないデコレータは次のように書きます。

@デコレータ関数
def デコレート対象の関数(引数):
    関数の中身

両者のちがいは、「 @ 」の後ろが「デコレータ関数を生成する関数(引数)」となっているか「デコレータ関数」となっているかです。

「デコレータ関数を生成する関数」と書いているのはまちがいではありません。引数を受け取れるデコレータというのは、実は「引数を受け取ってデコレータ関数を生成する関数」にほかなりません。

・・・概念的な説明だけだと何を言ってるのかわけわからないので具体例を見てみましょう。

sample.py:

# coding: utf-8

import os

def main():
    """main 関数
    """
    print("CSV generator.")
    names = None
    while not names:
        print("Enter file names with comma delimiter: ", end="")
        names = input().strip().split(",")

    create_files(*[name + ".csv" for name in names])

def add_print(pattern):
    """ファイルの作成前後に表示を行うデコレータ
    """
    def _add_print(func):
        def wrapper(*args):
            print("Creating {} {} ... ".format(pattern, ",".join(args)), end="")
            func(*args)
            print("Done!")
        return wrapper

    return _add_print

@add_print("files")
def create_files(*paths):
    """空のファイルを作成する
    """
    for p in paths:
        with open(p, "a"):
            os.utime(p, None)

if __name__ == "__main__":
    main()

動作イメージ:

python sample.py
CSV generator.
Enter file names with comma delimiter: a,b
Creating files a.csv,b.csv ... Done!

少し長いですが、ポイントになるのはごく一部です。

デコレータの使用場所は以下の部分です。

@add_print("files")
def create_files(*paths):
    """空のファイルを作成する
    """
    for p in paths:
        with open(p, "a"):
            os.utime(p, None)

この add_print() というのが上述の「引数を受け取ってデコレータ関数を生成する関数」になります。これはデコレータ関数ではなく、引数 files を受け取ってデコレータ関数 _add_print() を生成しています。そして、戻り値である _add_print() が create_files() を実際にデコレートし、 wrapper() という関数に置き換えてデコレート処理が完了するという流れです。

ということで、「引数を受け取れるデコレート関数」というものは実は見た目上のものであって、実際には「引数を受け取ってデコレート関数を生成する関数」を作成する必要があります。

元々デコレータ関数というもの自体が「関数を返す関数」なので、「デコレータを返す関数」となると自然「関数を返す関数を返す関数」という入れ子構造となります。見た目はとてもややこしいのですが、自分で実際に書いてみると構造がよくわかってすっきり見通せるようになるので、興味があってまだ書いたことのない方はよろしければぜひ自分の手でひとつ書いてみてください。

参考: