デコレータ

【 目次 】

デコレータを使って関数を別の関数でラップ(包み込む)する事で、関数の機能を拡張することができる。
デコレータの動作は単純で、ただ 関数関数を引数にとる別の関数 で変換してもとの関数と同じ名前の関数に置き換える事である。

以下のコードは、デコレータ関数dec_funcを使ってfunc関数の機能を拡張して、関数の実行の前と実行後に文字列の表示を追加する例である。
いわゆるロギングの機能を、もとの関数に追加している。

def dec_func(func):
    def wrapper_func(*list_args,**dict_args):
        u"ラッパー関数"
        print "before", func.__name__
        result=func(*list_args,**dict_args)
        print "after", func.__name__
        return result
    return wrapper_func

def func():
    u"デコレートされる関数"
    print u"もとの関数を実行"
func=dec_func(func)

func()

実行結果

before func
funcを実行
after func

wrapper_funcのなかでの関数funcの呼び出しのコードは、どのような引数を持つ関数でも処理できるようにfunc(*list_args,**dict_args)を使って任意の引数を受け取る関数として実装している。

このプログラムでは、デコレータ関数dec_funcを使って、もとの関数funcをwrapper_funcに置き換える事で、元の関数の機能を拡張している。
見方を変えると、デコレータは関数をフックして新たな機能を付け加えるものという事ができる。
AOP(アスペクト指向プログラミング)にもとづいたプログラミングをいとも簡単に実装できるアイデアという見方もできる。

デコレータはクロージャの応用で、内側の関数wrapper_funcはクロージャそして,外側のデコレータ関数dec_funcはエンクロージャになっている。

デコレータのアイデア自体は単純な発想で、一部の他の言語でも応用可能。
ちなに、JavaScriptでのコードを以下に示す。

function dec_func(func){
    return function(){
            console.log("関数実行前");
            func();
            console.log("関数実行後");
        };
}

function func(){
    console.log("もとの関数を実行");
}
func=dec_func(func);

func()

しかし、pythonならではの優れた点は@デコレータ関数というシンタックスシュガーが用意されている事。
上記のコードの関数funcの定義部分は以下のコードに置き換える事ができる。

@dec_func
def func():
    u"デコレートされる関数"
    print u"もとの関数を実行"

@dec_func

を関数定義の前に記述する事により自動的に

func=dec_func(func);

が実行される事になる。

と言葉をならべてみたが、なんとも説明しずらいので、他のサイトの参考記事をしめしておく。

functools.wraps

デコレータを使うと、もともとの関数のドキュメンテーション文字列や関数名は失われてしまい、ラッパー側の関数名や文字列に置き換えられてしまう。

print func.__name__
print func.__doc__

実行結果

wrapper_func
ラッパー関数

functoolsモジュールのwraps関数を使うと、これを防ぐ事ができる。

import functools

def dec_func(func):
    def wrapper_func(*list_args,**dict_args):
        u"ラッパー関数"
        print "before", func.__name__
        result=func(*list_args,**dict_args)
        print "after", func.__name__
        return result
    return functools.wraps(func)(wrapper_func)

もしくは、functools.wraps関数をwrapper_func関数のデコレータとして使って、

def dec_func(func):
    @functools.wraps(func)
    def wrapper_func(*list_args,**dict_args):
        u"ラッパー関数"
        print "before", func.__name__
        result=func(*list_args,**dict_args)
        print "after", func.__name__
        return result
    return wrapper_func

ドキュメンテーション文字列や関数名を表示してみると

print func.__name__
print func.__doc__

実行結果

func
デコレートされる関数

引数を持つデコレータ

引数を持つデコレータを定義する事ができる。

@dec_func_with_arg("xxx")
def func():
    print "もとの関数を実行"

これは、以下のコードと等価

func=dec_func_with_arg("xxx")(func)

引数を持つデコレータはやや難解。
う~ん!どのようなデコレータ関数を実装すればいいのだろうか、と悩んでしまったが。

引数付きの関数を用意して、その戻り値として引数なしのデコレータ関数を返せば良いようだ。
つまり、外側に引数を持つ関数をもう1つかぶせてデコレータ関数を返す関数を実装。

前述のdec_funcの外側にもう1つ関数をかぶせて

def dec_func_with_arg(arg):
    def dec_func(func):
        def wrapper_func(*list_args,**dict_args):
            print "before", func.__name__
            result=func(*list_args,**dict_args)
            print "after", func.__name__
            return result
        return wrapper_func
    return dec_func

ここでは、引数argは何の役割もはたしていないが。

では、デコレータの引数がどのように使えるかというと
次のコードは、指定の回数分、もとの関数を繰り返し実行する例である。

import functools

def dec_multiple_times_func(arg_times):
    def dec_func(func):
        def wrapper_func(*list_args, **dict_args):
            for i in xrange(arg_times) :
                result = func(*list_args, **dict_args)
            return result
        return functools.wraps(func)(wrapper_func)
    return dec_func

@dec_multiple_times_func(3)
def func():
    print u"もとの関数を実行"

func()

実行結果

もとの関数を実行
もとの関数を実行
もとの関数を実行

もう1つ、引数付きのデコレータの例を。
以下のコードは引数を複数指定して、もとの関数の実行前と後にそれぞれ別々の関数を実行する例である。

import functools

def dec_func_with_arg(befor_func, after_func):
    def dec_func(func):
        def wrapper_func(*list_args, **dict_args):
            befor_func()
            result = func(*list_args, **dict_args)
            after_func()
            return result
        return functools.wraps(func)(wrapper_func)
    return dec_func

def before_func():
    print u"before_funcを実行"

def after_func():
    print u"after_funcを実行"

@dec_func_with_arg(before_func, after_func)
def func():
    print u"もとの関数を実行"

func()

実行結果

before_funcを実行
もとの関数を実行
after_funcを実行

デコレータの引数を利用して、関数の実行前の処理と実行後の処理を、デコレートされる側の関数に対して個別に指定できることになり、よりキメの細かい処理が可能となる。

クラスによるデコレータの実装

クラスのインスタンスやメソッドを利用して、デコレータ関数を実装する事ができる。
クラスの属性やインスタンスの属性などを利用してクラスとしての機能を活用する事により、より高度なデコレータの実装の可能性が広がってくる。

callableなオブジェクトをデコレータとして使う。

クラスに特殊メソッド__call__を実装する事で、クラスのインスタンスをcallable(呼び出し可能)なオブジェクトとして実装する事ができる。

これを利用してクラスのインスタンスをデコレータとして使う事ができる。

import functools

class DecCls:
    def __call__(self, func):
        @functools.wraps(func)
        def wrapper_func(*list_args,**dict_args):
            u"ラッパー関数"
            print "before", func.__name__
            result=func(*list_args,**dict_args)
            print "after", func.__name__
            return result
        return wrapper_func

dec_func=DecCls()

@dec_func
def func():
    u"デコレートされる関数"
    print u"もとの関数を実行"

func()

実行結果

before func
もとの関数を実行
after func

メソッドをデコレータとして使う。

クラスに特殊メソッド__call__を実装するのでは無く、単純にデコレータ関数としてクラスのメソッドを利用する事もできる。
以下はインスタンスメソッドをデコレータ関数として使う例である。

import functools

class DecCls:
    def method(self, func):
        @functools.wraps(func)
        def wrapper_func(*list_args,**dict_args):
            u"ラッパー関数"
            print "before", func.__name__
            result=func(*list_args,**dict_args)
            print "after", func.__name__
            return result
        return wrapper_func

dec_func=DecCls()

@dec_func.method
def func():
    u"デコレートされる関数"
    print u"もとの関数を実行"

ここではインスタンスメソッドを使ってデコレータ関数を実装したが、クラスメソッドやスタティックメソドも利用可能。

複数のデコレータを指定

デコレータを複数指定する事も可能で、その場合には関数の定義の近くに指定されたデコレータから順番にデコレートされる事になる。

@outer_deco_func
@inner_deco_func
def func():
    pass

は、以下のコードと等価になる。

func=outer_deco_func(inner_deco_func(func))

前述のdec_func関数を複数指定すると

@dec_func
@dec_func
def func():
    print u"もとの関数を実行"

func()

実行結果

before wrapper_func
before func
もとの関数を実行
after func
after wrapper_func

関数の実行前と後に、2重に関数がフックされている事がわかる。


その他の参考記事。

ページのトップへ戻る