Python デコレータの実装原理とメタデータ保存テクニック

Python 言語においては関数もオブジェクトとして扱われ、変数に対して代入することが可能です。この特性により、関数名を経由せずとも対象のコードを実行することができます。

関数オブジェクトには __name__ といった属性が存在し、これによって関数名を取得できます。

ここで、既存の関数の実装を直接変更することなく、呼び出しの前後に自動的なログ記録などの機能を付与したいケースを考えます。コード実行の時点での動的な機能拡張を実現する手法が「デコレータ(Decorator)」と呼ばれます。

基本となるデコレータの実装

構造的に見ると、デコレータは入力として関数を受け取り、強化された新たな関数を返却する高階関数です。以下に、簡易的なログ出力を行うデコレータの定義例を示します。

def execution_trace(original_method):
    def intercepted_call(*params, **options):
        print(f'Executing: {original_method.__name__}')
        return original_method(*params, **options)
    return intercepted_call

上記の execution_trace はデコレータとして機能するため、第一引数に関数を期待し、内部でラッパー関数を生成して返却します。Python が提供する @ 記法を使用することで、デコレータを関数定義直後に配置することが可能です。

@execution_trace
def sample_task():
    print('Running main task logic')

この設定下で sample_task() を呼び出すと、本来の処理が始まる前にログメッセージが表示されます。

文法上、@execution_trace を付け加えることは、以下の式を実行したのと同等の意味を持ちます。

sample_task = execution_trace(sample_task)

元の sample_task 自体はメモリ上に残存していますが、名前空間内の識別子 sample_task が指す先が、デコレータから返された新しい関数(上記例の intercepted_call)へと置き換わります。

引数付きデコレータの構造

デコレータ自体にパラメータを渡す必要がある場合、さらに高いレベルの関数を戻り値とする構成が必要です。カスタムテキストを設定するようなケースでは、ネスト構造が 3 段階になります。

def configure_logging(prefix_label):
    def inner_decorator(original_method):
        def interceptor(*args, **kwargs):
            print(f'{prefix_label} - {original_method.__name__}')
            return original_method(*args, **kwargs)
        return interceptor
    return inner_decorator

このような三重構造化されたデコレータの使用例は以下の通りです。

@configure_logging('Info Log:')
def data_process():
    print('Processing data...')

評価順序は、まず configure_logging が呼ばれ結果として inner_decorator が得られ、それが実際の対象関数を引数として受け取り、最終的に interceptor を返すというフローとなります。

関数メタデータの保持問題

前述の実装には潜在的な課題があります。関数はオブジェクトであり特定の属性を持ちますが、デコレータ適用後の関数が参照する __name__ などは、元の名前ではなく内側にあるラッパー関数の名前(例:interceptor)になってしまう現象が発生します。

この性質が変わってしまうと、リフレクションやドキュメント生成など関数シグネチャに依存するシステム内でエラーを引き起こす可能性があります。

手動で属性をコピーする必要はありません。標準ライブラリの functools モジュールに含まれる wraps 機能がこれを解決します。修正済みの完全な実装は以下のようになります。

import functools

def execution_trace(original_method):
    @functools.wraps(original_method)
    def intercepted_call(*params, **options):
        print(f'Executing: {original_method.__name__}')
        return original_method(*params, **options)
    return intercepted_call

パラメータを受け取るタイプの場合も同様に適用可能です。

import functools

def configure_logging(prefix_label):
    def inner_decorator(original_method):
        @functools.wraps(original_method)
        def interceptor(*args, **kwargs):
            print(f'{prefix_label} - {original_method.__name__}')
            return original_method(*args, **kwargs)
        return interceptor
    return inner_decorator

import functools によってモジュールをインポートした後、ラッパー関数の定義直前で @functools.wraps(対象関数) を付与することで、元のメタ情報が正しく継承されます。

タグ: Python decorator functools high-order-function

5月29日 17:12 投稿