Pythonデコレータの仕組みと実践パターン

Pythonデコレータの基本的な仕組みから、引数付きデコレータ、クラスデコレータ、functools.wrapsの活用まで、実践的なパターンをコード例とともに解説します。

はじめに

デコレータは、既存の関数やクラスの動作を変更せずに機能を追加するPythonの強力な仕組みです。@ 構文で宣言的に適用でき、ログ記録、実行時間計測、リトライ処理、キャッシュなど横断的関心事の実装に広く使われています。

本記事では、デコレータの基礎から実践的なパターンまでをコード例とともに解説します。

前提知識:第一級関数とクロージャ

Pythonでは関数はオブジェクトであり、変数に代入したり、他の関数に渡したり、関数から返したりできます。

def greet(name):
    return f"Hello, {name}"

say_hello = greet  # 関数を変数に代入
print(say_hello("Alice"))  # "Hello, Alice"

クロージャは、外側の関数のスコープにある変数を参照する内部関数です。外側の関数が終了した後も、その変数にアクセスできます。

def make_multiplier(factor):
    def multiplier(x):
        return x * factor  # factorを参照(クロージャ)
    return multiplier

double = make_multiplier(2)
print(double(5))  # 10

基本的なデコレータ

デコレータは「関数を受け取り、関数を返す関数」です。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

# @my_decorator は add = my_decorator(add) と等価
print(add(3, 4))
# 出力:
# Calling add
# Finished add
# 7

functools.wraps の重要性

デコレータを適用すると、元の関数のメタデータ(__name____doc__)がラッパー関数のものに置き換わります。functools.wraps でこれを防ぎます。

import functools

def my_decorator(func):
    @functools.wraps(func)  # 元の関数のメタデータを保持
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def add(a, b):
    """二つの数を足す"""
    return a + b

print(add.__name__)  # "add"(wrapsなしだと"wrapper")
print(add.__doc__)   # "二つの数を足す"

引数付きデコレータ

デコレータ自体に引数を渡したい場合、3重のネスト構造になります。

import functools

def repeat(n):
    """関数をn回繰り返すデコレータ"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(n):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))  # ["Hello, Alice", "Hello, Alice", "Hello, Alice"]

実践パターン

実行時間計測

import functools
import time

def timer(func):
    """関数の実行時間を計測するデコレータ"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "done"

slow_function()  # "slow_function: 1.0012s"

リトライ(指数バックオフ付き)

import functools
import time

def retry(max_attempts=3, base_delay=1.0):
    """失敗時にリトライするデコレータ(指数バックオフ)"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    delay = base_delay * (2 ** attempt)
                    print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, base_delay=0.5)
def unreliable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("API unavailable")
    return {"status": "ok"}

簡易キャッシュ

import functools

def simple_cache(func):
    """結果をキャッシュするデコレータ"""
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

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

print(fibonacci(100))  # キャッシュなしだと非現実的な計算時間

実用的には functools.lru_cache が同等の機能を提供します。

@functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

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

__call__ メソッドを実装したクラスもデコレータとして使えます。状態を保持したい場合に有用です。

import functools

class CountCalls:
    """関数の呼び出し回数をカウントするデコレータ"""
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
print(f"Called {say_hello.count} times")  # "Called 2 times"

デコレータのスタック

複数のデコレータは下から上に適用され、実行時は上から下に呼ばれます。

@timer
@retry(max_attempts=2)
def api_call():
    pass

# 等価: api_call = timer(retry(max_attempts=2)(api_call))
# 実行時: timer → retry → api_call

組み込みデコレータ

Python標準の代表的なデコレータです。

デコレータ用途
@propertyメソッドをプロパティとしてアクセス
@staticmethodインスタンス不要のメソッド
@classmethodクラスを第一引数に受けるメソッド
@functools.lru_cache結果のメモ化キャッシュ
@functools.wrapsデコレータ内でメタデータを保持
@dataclasses.dataclassデータクラスの自動生成

関連記事

参考文献