CLOVER🍀

That was when it all began.

PythonでJavaのProxyライクにAOP

前回は関数デコレータでお手軽にAOPっぽいことをやってみましたが、これだと実体のクラス/メソッド定義そのものに織り込むコードを書かないといけないのであまりいい感じがしません。

だったら、やっぱりクラスベースで動的に織り込みますかってことで、JavaのProxyライクなクラスを軽い気持ちで書いてみました。

軽い気持ちで書いたはずだったんですけど…これがけっこう手間になったんですよねぇ…。

まずは、プロキシ側のモジュール一式。
aop_proxy.py

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

import re

# Interceptorのメソッドチェーンを作成するためのクラス
class MethodInvocation(object):
    def __init__(self, current, next):
        self.current = current
        self.next = next
        self.method = None

    def invoke_ready(self, method):
        self.method = method
        if self.next != None:
            self.next.invoke_ready(method)
        else:
            self.current = method

    def invoke(self, *args, **keyword):
        if self.next != None:
            return self.current.invoke(self.next, *args, **keyword)
        else:
            # call original method
            return self.current(*args, **keyword)

# Interceptorの基底クラス。基本的に、何もしない
class Interceptor(object):
    def __init__(self): pass
    def can_pre_weave(self, method, *args, **keyword): return False
    def pre_invoke(self, method, *args, **keyword): pass
    def invoke(self, method_invocation, *args, **keyword):
        return method_invocation.invoke(*args, **keyword)
    def can_post_weave(self, method, result, *args, **keyword): return False
    def post_invoke(self, method, result, *args, **keyword): pass

# 本来のメソッド呼び出す前後に処理を織り込むか否かを、
# コンストラクタで与えられた正規表現で判定するInterceptor
class RegexPrePostInterceptor(Interceptor):
    def __init__(self, regex):
        super(RegexPrePostInterceptor, self).__init__()
        self.pattern = re.compile(regex)

    def can_pre_weave(self, method, *args, **keyword):
        return self.pattern.match(method.im_func.__name__) != None

    def can_post_weave(self, method, result, *args, **keyword):
        return self.can_pre_weave(method, *args, **keyword)

# 実際の処理の前後にログ出力を行うInterceptor
class LoggingInterceptor(RegexPrePostInterceptor):
    def __init__(self, regex):
        super(LoggingInterceptor, self).__init__(regex)

    def pre_invoke(self, method, *args, **keyword):
        print "Before Call method[%s.%s], args[%s, %s]" % (method.im_class.__name__, method.__name__, args, keyword)

    def post_invoke(self, method, result, *args, **keyword):
        print "After Call method[%s.%s], result[%s], args[%s, %s]" % (method.im_class.__name__, method.__name__, result, args, keyword)

# 本来のクラスのインスタンスのプロキシとなるクラス
# Interceptorを噛ませた後に、元々の処理対象へリクエストを転送する
class Proxy(object):
    def __init__(self, delegate, interceptors):
        self.delegate = delegate
        self.interceptors = interceptors

        self.chain_last = MethodInvocation(None, None)
        if interceptors == []:
            self.invocation_chain = self.chain_last
        else:
            reverse_interceptors = interceptors[::-1]
            current = self.chain_last
            for interceptor in reverse_interceptors:
                current = MethodInvocation(interceptor, current)
            self.invocation_chain = current
                
    def __getattr__(self, attrname):
        method = getattr(self.delegate, attrname)

        # クライアントコードがメソッド呼び出しを行う際に、呼び出す関数
        def wrapped(*args, **keyword):
            for interceptor in self.interceptors:
                if interceptor.can_pre_weave(method, *args, **keyword):
                    interceptor.pre_invoke(method, *args, **keyword)

            self.invocation_chain.invoke_ready(method)
            result = self.invocation_chain.invoke(*args, **keyword)

            for interceptor in self.interceptors:
                if interceptor.can_post_weave(method, result, *args, **keyword):
                    interceptor.post_invoke(method, result, *args, **keyword)

            return result
        
        return wrapped

余計にいろいろ機能を書いたせいで、妙に豪快な構成に…。

中心となっているのはProxyクラスです。インスタンス生成時に、引数delegateでAOPで処理を織り込む対象のクラスのインスタンスを受け取り、引数interceptorsで織り込むInterceptorのリストを受け取ります。

呼び出し側のコードは以下の用になります。

proxied.py

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

from aop_proxy import Proxy, LoggingInterceptor

class Greeting(object):
    def __init__(self): pass
    def greeting(self, word):
        print "Hello %s!" % word
    def say(self): print "Hello World"

if __name__ == "__main__":
    g = Proxy(Greeting(), [LoggingInterceptor("greeting.*")])
    g.greeting("World")
    g.greeting(word = "Python")
    g.say()

AOPを適用している側は、生成したインスタンスをすぐさまProxyのコンストラクタに渡し、Proxyのインスタンスをあたかも大元のクラスのように使用します。なお、AOPはログ出力機能を適用し、greetingを含むメソッド名の場合に処理を織り込むようにします。

実行すると、こういう動きをします。

$ python proxied.py 
Before Call method[Greeting.greeting], args[('World',), {}]
Hello World!
After Call method[Greeting.greeting], result[None], args[('World',), {}]
Before Call method[Greeting.greeting], args[(), {'word': 'Python'}]
Hello Python!
After Call method[Greeting.greeting], result[None], args[(), {'word': 'Python'}]
Hello World

結果、greetingメソッドの呼び出しにはログ出力が織り込まれていますが、sayメソッドには適用されていません。

この仕組みは、Pythonの演算子のオーバーロードのうち、「__getattr__」を利用しています。これは属性へのアクセスリクエストが行われた際に呼び出されるメソッドで、このメソッドからwrappedメソッドを返すことにより元々の処理をwrappedメソッド経由で呼び出しています。

要は、この部分ですね。

    # Code From Proxy Class...
    def __getattr__(self, attrname):
        method = getattr(self.delegate, attrname)

        def wrapped(*args, **keyword):
            for interceptor in self.interceptors:
                if interceptor.can_pre_weave(method, *args, **keyword):
                    interceptor.pre_invoke(method, *args, **keyword)

            self.invocation_chain.invoke_ready(method)
            # Call Interceptor Method Chain
            result = self.invocation_chain.invoke(*args, **keyword)

            for interceptor in self.interceptors:
                if interceptor.can_post_weave(method, result, *args, **keyword):
                    interceptor.post_invoke(method, result, *args, **keyword)

            return result
        
        return wrapped

ああ、フィールドからの値の取得/代入には効きませんよ。あと考慮もしていません。

Proxyクラスのインスタンス生成時にInterceptorのリストを渡しますが、Interceptorは積み重ねが可能なようにしています。本来の処理を呼び出すのはInterceptor#invokeメソッドですが、処理を織り込むメソッド自身ではなくMethodInvocationクラスを介して呼び出すようにしています。

つまり、こいつですね。

class MethodInvocation(object):
    def __init__(self, current, next):
        self.current = current
        self.next = next
        self.method = None

    # 省略…

    def invoke(self, *args, **keyword):
        if self.next != None:
            return self.current.invoke(self.next, *args, **keyword)
        else:
            # call original method
            return self.current(*args, **keyword)

class Interceptor(object):
    # 省略…
    def invoke(self, method_invocation, *args, **keyword):
        return method_invocation.invoke(*args, **keyword)

これをProxyインスタンスの構築時に、MethodInvocationクラスのチェーンとして作成します。

        # Code From Proxy Class...
        self.chain_last = MethodInvocation(None, None)
        if interceptors == []:
            self.invocation_chain = self.chain_last
        else:
            reverse_interceptors = interceptors[::-1]
            current = self.chain_last
            for interceptor in reverse_interceptors:
                current = MethodInvocation(interceptor, current)
            self.invocation_chain = current

チェーンの最後は本来のメソッドになるので、積み重ねられたInterceptorはMethodInvocation#invokeメソッドを呼び出すと次のInterceptorを呼び出し、もし次のInterceptorがなければ本来のメソッドを呼び出す、という仕掛けになっています。

Interceptorに直接本来のメソッドを渡してしまうと、同じ処理を何回も呼び出すことになってしまいますからねぇ。

と、軽い気持ちで始めた割にはけっこう壮大なことに。ここまでやるなら、もうちょっとインターフェースを考えればよかった…。InterceptorをPre/Postにも織り込めるようにしたつもりなんですけど、メソッド自体を包むinvokeとの境界がちょっと曖昧に…。