Home -> Python - デコレータ

Python - デコレータ

JavaScriptが有効になっていないため目次を見ることができません。

デコレータとは何か

まずはじめに、実際にデコレータを使用したプログラムを見てみましょう。

def deco(func):
    print("deco")
    return "string"

@deco
def test():
    pass

実行結果

deco

このプログラムでは、「まだ関数を定義しただけなのに、どうして"deco"と出力されるんだ」という疑問を持たれるかもしれません。それには理由があります。

test関数の定義をしている部分に注目してください。

@deco
def test():
    pass

これは、以下と等価です。

def test():
    pass
test = deco(test)

これで先ほどの疑問は解消されました。そうです、deco関数が呼び出されていたのです。そして、testはもはや関数ではないこともわかります。testは文字列です。

デコレータは、デコレートする関数定義の手前で前もって関数を置き換えることができます。

関数をデコレートする

デコレータは一般的に関数をデコレート(装飾)するために用いられます。言い換えると、関数に付加機能をつけるということです。以下に関数の実行速度を表示するようデコレートする例を示します。

import time

def instrument(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, ":", end - start)
        return result
    return wrapper

@instrument
def foo():
    time.sleep(1)

foo()

実行結果

foo : 1.0

この例はクロージャを使用していますが、その説明はしません。wrapper関数では、funcをコールしています。このfuncはinstrument関数のローカル変数です。1行目でtimeをインポートしているにもかかわらずinstrument関数でtimeをインポートしているのは、関数の再利用を考慮しているためです。

ここで注目すべきことは、デコレータを使う側ではプログラムがより見やすいことです。

属性の置き換えに注意する

デコレータは、関数を置き換えることができると前述しました。それは、関数の属性が置き換わることも意味します。

先ほどの例のfoo関数の名前を見てみます。

print(foo.__name__)

実行結果

wrapper

クライアントは関数名が置き換わることを期待していません。クライアントの期待通りに動くプログラムを以下に示します。

import time

def instrument(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, ":", end - start)
        return result
    return wrapper

@instrument
def foo():
    time.sleep(1)

print(foo.__name__)

実行結果

foo

wraps関数は、デコレートする関数の属性をラッパ関数に引き継いでいます。

デコレータに引数を渡す

デコレータには引数を渡すことが可能です。以下は、指定した引数へ関数の戻り値を変更するようデコレートするプログラムを以下に示します。

def change_result(result):
    def receive_func(func):
        import functools
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            return result
        return wrapper
    return receive_func

@change_result(False)
def foo():
    return True

print(foo())

実行結果

False

foo関数に注目してください。

@change_result(False)
def foo():
    return True

これは、以下と等価です。

def foo():
    return True
foo = change_result(False)()

更に分解するとこのようになります。

def foo():
    return True
tmp = change_result(False)
foo = tmp()

先ほどの例に比べてインデントがひとつ深くなりました。デコレータの作成はプログラムが複雑になりがちです。注意深く実装しましょう。

デコレータのネスト

デコレータをネストすることができます。以下に例を示します。

@d1
@d2
def func():
    pass

これは、以下と等価です。

def func():
    pass
func = d1(d2(func))

クロージャの代わりに呼び出し可能(callable)なクラスインスタンスを使用する

関数をコールしたときに引数を表示するようデコレートするプログラムを以下に示します。

class PrintArgs(object):
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print(*args, **kwargs)
        return self.func(*args, **kwargs)

def print_args(func):
    return PrintArgs(func)

@print_args
def avg(numlist):
    return sum(numlist) / len(numlist)

avg(range(100))

実行結果

range(0, 100)
20

この例は呼び出し可能なクラスインスタンスを使用していますが、その説明はしません。

呼び出し可能なオブジェクトであれば関数以外でも使用できますが、基本的には関数を使用することを推奨します。

まとめ

デコレータのメリット

デコレータのデメリット