Pythonでデコレーターを自作する方法

プログラミング

Pythonにおけるカスタムデコレーターの作成方法

Pythonのデコレーターは、既存の関数やメソッドの振る舞いを変更・拡張するための強力な機能です。デコレーターは、高階関数(関数を引数として受け取り、関数を返す関数)の概念に基づいています。これにより、コードの重複を避け、関数のロジックをよりクリーンに保つことができます。このドキュメントでは、Pythonでカスタムデコレーターを自作する方法について、その仕組みと応用例を詳細に解説します。

デコレーターの基本構造

Pythonにおけるデコレーターは、一般的に以下の構造を持っています。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # デコレーターの処理(関数実行前)
        print("関数が呼び出される前です。")
        result = func(*args, **kwargs)
        # デコレーターの処理(関数実行後)
        print("関数が呼び出された後です。")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("こんにちは!")

say_hello()

この例では、`my_decorator` という関数がデコレーターとして機能します。

  • `my_decorator` 関数は、デコレートされる対象の関数 (`func`) を引数として受け取ります。
  • `my_decorator` 関数は、内部で `wrapper` という別の関数を定義し、それを返します。この `wrapper` 関数が、元の関数 `func` をラップ(包み込み)ます。
  • `wrapper` 関数は、`*args` と `**kwargs` を使用して、元の関数が受け取る任意の引数とキーワード引数をすべて受け取れるようにしています。これにより、どのような引数を持つ関数でもデコレートできるようになります。
  • `wrapper` 関数内では、元の関数 `func` を呼び出す前後に、追加したい処理を記述できます。
  • 最終的に、`wrapper` 関数は、元の関数 `func` の実行結果 (`result`) を返します。
  • `@my_decorator` という記法は、`say_hello` 関数を `my_decorator` でデコレートすることをPythonインタープリターに伝えます。これは、実際には `say_hello = my_decorator(say_hello)` というコードと同じ意味になります。

この基本構造を理解することが、カスタムデコレーターを作成する上での第一歩となります。

デコレーターの応用例

デコレーターは、様々な場面で活用できます。以下にいくつかの代表的な応用例を紹介します。

1. 実行時間の計測

関数が実行にかかった時間を計測したい場合にデコレーターは非常に役立ちます。

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} は {end_time - start_time:.4f} 秒で実行されました。")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(2)
    print("処理が完了しました。")

slow_function()

このデコレーターは、関数実行前後に `time.time()` を呼び出し、その差分を計算して表示します。`func.__name__` を使うことで、どの関数が計測されたのかを明確にすることができます。

2. アクセス制御(認証・認可)

特定の関数へのアクセスを、認証されたユーザーのみに制限したい場合などに利用できます。

def require_login(func):
    def wrapper(*args, **kwargs):
        if is_user_logged_in():  # 実際にはログイン状態をチェックするロジック
            return func(*args, **kwargs)
        else:
            print("ログインが必要です。")
            # 必要であればリダイレクトやエラーレスポンスを返す
    return wrapper

def is_user_logged_in():
    # ここに実際のログイン状態を判定するロジックを実装
    # 例: return session.get("user_id") is not None
    return False # デモのため常にFalseとする

@require_login
def view_dashboard():
    print("ダッシュボードが表示されました。")

view_dashboard()

`is_user_logged_in()` は、実際のアプリケーションではセッション情報などを参照してユーザーのログイン状態を判定する関数に置き換える必要があります。

3. キャッシュ(Memoization)

計算コストの高い関数に対して、一度計算した結果を保存しておき、同じ引数で再度呼び出された際に計算せずに保存しておいた結果を返すことで、パフォーマンスを向上させる技法です。

def memoize(func):
    cache = {}
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key in cache:
            print(f"キャッシュから結果を返します: {key}")
            return cache[key]
        else:
            print(f"計算してキャッシュに保存します: {key}")
            result = func(*args, **kwargs)
            cache[key] = result
            return result
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))
print(fibonacci(10)) # 2回目はキャッシュから取得される

この例では、辞書 (`cache`) を使って、引数と結果のペアを保存しています。引数はタプルとしてキーにする必要があります。キーワード引数も考慮するために、`kwargs.items()` をソートしてタプルに変換しています。

デコレーターの引数

デコレーター自身に引数を渡したい場合もあります。その場合は、デコレーターを返す関数をさらに一つ追加する必要があります。

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"こんにちは、{name}さん!")

greet("太郎")

この構造は、以下のようになります。

  1. `repeat(num_times)`: デコレーターに渡される引数を受け取る関数。
  2. `decorator_repeat(func)`: デコレートされる関数を受け取る関数(標準的なデコレーターの構造)。
  3. `wrapper(*args, **kwargs)`: 実際に元の関数をラップする関数。

`@repeat(num_times=3)` は、`greet = repeat(num_times=3)(greet)` というコードに展開されます。

`functools.wraps` の利用

デコレーターを使用すると、元の関数のメタ情報(`__name__`、`__doc__` など)が失われてしまうことがあります。これは、デコレーターが返す `wrapper` 関数のメタ情報が優先されるためです。この問題を解決するために、`functools` モジュールの `wraps` デコレーターを使用します。

import functools

def my_simple_decorator(func):
    @functools.wraps(func) # この行を追加
    def wrapper(*args, **kwargs):
        print("ラップしています。")
        return func(*args, **kwargs)
    return wrapper

@my_simple_decorator
def my_function_with_meta():
    """これはテスト用の関数です。"""
    pass

print(my_function_with_meta.__name__)
print(my_function_with_meta.__doc__)

`@functools.wraps(func)` を `wrapper` 関数に適用することで、`wrapper` 関数は元の関数 `func` のメタ情報を引き継ぐようになります。これにより、デバッグやドキュメンテーション生成が容易になります。

クラスベースのデコレーター

関数ベースのデコレーターが一般的ですが、クラスを使ってデコレーターを作成することも可能です。クラスベースのデコレーターは、状態(変数など)を保持したい場合に便利です。

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func) # メタ情報を引き継ぐ
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"'{self.func.__name__}' は {self.num_calls} 回呼び出されました。")
        return self.func(*args, **kwargs)

@CountCalls
def say_whee():
    print("ウィー!")

say_whee()
say_whee()

このクラスベースのデコレーターでは、以下のようになります。

  • `__init__(self, func)`: デコレートされる関数 `func` を受け取り、`self.func` に保持します。`functools.update_wrapper` を使ってメタ情報を引き継いでいます。
  • `__call__(self, *args, **kwargs)`: インスタンスが関数のように呼び出されたときに実行されます。ここで呼び出し回数をカウントし、元の関数 `self.func` を実行します。

クラスベースのデコレーターは、より複雑な状態管理や、デコレーター自体の振る舞いをカスタマイズしたい場合に強力な選択肢となります。

まとめ

Pythonのデコレーターは、コードの再利用性と可読性を高めるための非常に有用なツールです。基本構造を理解し、`functools.wraps` を活用することで、より堅牢で使いやすいデコレーターを作成できます。実行時間の計測、アクセス制御、キャッシングなど、様々な応用例があり、Pythonicなコーディングスタイルを実践する上で欠かせない要素と言えるでしょう。関数ベースのデコレーターとクラスベースのデコレーターの特性を理解し、状況に応じて適切な方を選択することが重要です。