yoraba build

備忘録を兼ねた技術ブログ

アスペクト指向クラスを作成しました。

アスペクト指向のライブラリは数ありますが、
デファクトスタンダードがどれか解らず、
環境によってinstallに失敗してしまうという問題もあるので、
勉強がてら自作しました。
苦戦したポイントなどを含めて解説します。

アスペクト指向抽象クラス

まず、処理フローについて検討します。
アスペクト指向で行いたい処理は大体以下のようなものでしょう

  • 関数実行前処理
  • 関数実行後処理
  • 例外発生時処理

これらの処理を抽象メソッドとして定義しておき、
サブクラスで実装を行うことで、多態性を持たせることが出来ます。
かかる抽象クラスはクラスデコレータとして扱います。
よって、__call__メソッド内にデコレータコール時の処理を書きます。

from abc import ABCMeta, abstractmethod


class Aspect(metaclass=ABCMeta):
    """アスペクト指向抽象クラス"""

    @abstractmethod
    def pre_process(self, *args, **kwargs):
        pass

    @abstractmethod
    def after_process(self, result):
        pass

    @abstractmethod
    def raise_process(self, e: Exception):
        pass

    def __call__(self, func: object):
        # サブクラスから修飾対象メソッドの属性を取得できるように保持します
        self.func = func

        def wrapper(*args, **kwargs):
            self.pre_process(*args, **kwargs)
            try:
                result = func(*args, **kwargs)
            except Exception as e:
                self.raise_process(e)
            self.after_process(result)
            return result
        return wrapper

クラスデコレータには引数を受け取るものと、受け取らないものがあります。
上記は引数を受け取るクラスデコレータです。

引数を受け取るcallableクラスデコレータ

デコレータに渡されるパラメータをコンストラクタの仮引数とします。
__call__には修飾対象メソッドが渡され、
ローカル関数に修飾対象メソッドの実引数が渡されます。
利用側では、@decorator(引数)のように書きます。

引数を受け取らないcallableクラスデコレータ

コンストラクタに修飾対象メソッドが渡されます。
__call__に修飾対象メソッドの実引数が渡されます。
利用側では、@decoratorのように書きます。

当初は引数を受け取らないクラスデコレータとして作成しようとしたのですが、
selfの扱いに困り、引数を受け取るクラスデコレータを選択しました。
引数を受け取るクラスデコレータにおいては、
ローカル関数の可変長引数である*argsの第一要素に呼び出し元のselfが入ってくるという点がポイントです。

__call__内での修飾対象メソッド呼び出しは間接的な呼び出しとなるため、第一要素にselfが自動的に渡されるということがないようです。
selfが入ってこないと、修飾対象メソッド内でselfを利用できなくなります。
つまり修飾対象メソッドが所属しているクラスのインスタンス関数やインスタンス変数を一切使えなくなってしまいます。
そして、抽象クラス側から、呼び出し元クラスのselfを参照する手段も見つかりませんでした。
呼び出し元クラス側からselfが渡されることを期待するしかなかったのです。
よってローカル関数にselfを渡してくれる、引数を受け取るクラスデコレータの利用に至りました。

アスペクト指向サブクラス

サブクラスで抽象メソッドをオーバーライドし、ログ処理など、必要な処理を書いていきます。
処理が不要な関数に関してはpassと書けば、必要な処理だけを部分的に実装することが出来ます。

import aspects.aspect as aop


class AOPPrinter(aop.Aspect):

    def pre_process(self, *args, **kwargs):
        from datetime import datetime
        print("START", self.func.__module__, self.func.__name__, datetime.now(), args, kwargs, sep='\t')

    def after_process(self, result):
        from datetime import datetime
        print("END", self.func.__module__, self.func.__name__, datetime.now(), result, sep='\t')

    def raise_process(self, e: Exception):
        from datetime import datetime
        print("RAISE", self.func.__module__, self.func.__name__, datetime.now(), e, sep='\t')

利用側

サブクラスをデコレータとして指定します。
引数ありデコレータとして扱うので末尾に括弧を書いています。
selfを要するインスタンスメソッドでも、selfを要しない静的なメソッドでも同様に修飾可能です。

@AOPPrinter()
def static_method():
    print("Execute")


class TestAOPPrinter():

    def self_method(self):
        self.self_value = "Self value"
        print(self.self_value)

    @AOPPrinter()
    def instance_method(self, a, b, name="", value=""):
        print("Execute")
        # selfが渡されないと以下のような参照が出来ません
        self.self_method()
        print(a, b, name, value)
        return a + b