CLOVER🍀

That was when it all began.

関数デコレータで、AOPっぽく

なんとなく、PythonでAOPっぽいことをやってみたくなりまして。実現方法を考えてみましたが、1番簡単なのが関数デコレータを使うことかなぁと。

関数デコレータは、文字通り関数を拡張するものです。まあ、とりあえず書いてみましょう。AOP適用の例としては、簡単にbefore/afterログで。

aop_decorator.py

#/usr/local/python
# -*- coding: utf8 -*-

def trace(func):
    def _trace(*args, **keyword):
        print 'Called [%s], args[%s, %s] before' % (func.__name__, args[1:], keyword)
        result = func(*args, **keyword)
        print 'Called [%s], args[%s, %s] after' % (func.__name__, args[1:], keyword)
        return result
    return _trace

ごく普通の関数ですが、これを利用して既存の関数を拡張することができます。なお、適用する関数はどんな引数を受け取るかわからないので、可変長引数*args, **keywordを利用しており、関数を返す関数にもなっています。

動作としては、この関数デコレータを適用した関数が呼び出された時に、その関数名と引数を出力するようになります。

適用例は、以下のコードです。

decorated.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from aop_decorator import trace

class Greeting(object):
    def __init__(self, name):
        self.name = name

    @trace
    def greet_trace(self, target):
        print "Hello %s to %s" % (self.name, target)

    def greet(self):
        print "Hello %s!" % self.name

if __name__ == "__main__":
    g = Greeting("Python")
    g.greet_trace("AOP")
    g.greet()

Greetingクラスのgreet_traceメソッドに、@traceで関数デコレータを付与しています。これで、関数デコレータを適用したことになります。

実行すると、こうなります。

$ python decorated.py 
Called [greet_trace], args[('AOP',), {}] before
Hello Python to AOP
Called [greet_trace], args[('AOP',), {}] after
Hello Python!

一応、ちゃんと動作しています。当然ですが、関数デコレータを付与していないメソッドには、何の効果もありません。

さっきの関数デコレータをもっかい見てみましょう。

def trace(func):
    def _trace(*args, **keyword):
        print 'Called [%s], args[%s, %s] before' % (func.__name__, args[1:], keyword)
        result = func(*args, **keyword)
        print 'Called [%s], args[%s, %s] after' % (func.__name__, args[1:], keyword)
        return result
    return _trace

実際に動く時には、trace関数の引数に本来の呼び出し対象の関数/メソッドが渡ってきます。これを自由変数として取り込んだ内部関数_traceを返却すると、外側からは本来呼び出したい関数を呼び出すと実際には_trace関数が呼び出されます。その時に、渡ってきた引数argsおよびkeywordを引数にfuncを呼び出すという仕掛けです。

よって、実体の関数を呼び出しているのは、以下の部分です。

        result = func(*args, **keyword)

なお、関数デコレータをクラスとして宣言する試みもやってみたのですが、こちらはうまくいかず…。

class trace_class(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **keyword):
        print 'Called [%s], args[%s, %s] before' % (self.func.__name__, args, keyword)
        self.func(*args, **keyword)  # これでは動かない…
        print 'Called [%s], args[%s, %s] after' % (self.func.__name__, args, keyword)

こんな感じで試してみたんですけど、残念ながら動作しません。対象のメソッドを呼び出す際に、相手のオブジェクト参照selfが足りず、エラーになってしまいます。これをどうやって取得したらいいのかが、ちょっとわかりませんでした…。