デコレータとクロージャ

デコレータは Python の最もエレガントな機能の 1 つです。元のコードを変更せずに関数に「追加機能」を付け加えられます——ログ追加、タイミング、権限チェック……多くのフレームワークやライブラリがデコレータを広く使用しています。デコレータを理解することは、Python 習得への重要なマイルストーンです。


1. クロージャ

クロージャはデコレータの基礎です。簡単に言うと:内部関数が、外部関数の実行が終了した後でも、外部関数の変数を覚えていることです。

PYTHON
def make_multiplier(n):
    """乗算関数を作成"""
    def multiplier(x):
        return x * n        # 内部関数が外部の変数 n を覚えている
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))            # 10
print(triple(5))            # 15
print(double(10))           # 20

doubletriple は同じファクトリ関数 make_multiplier から来ていますが、それぞれ異なる n の値(2 と 3)を覚えています。これがクロージャです——関数 + それが覚えている外部変数です。

💡 クロージャのユースケース: 1)ファクトリ関数(上記のように異なる設定の関数を作成)。2)デコレータ(標準的な実装)。3)コールバック関数でのコンテキスト保存。


2. デコレータの基本

デコレータは関数を引数として受け取り、新しい関数を返す関数です:

PYTHON
def my_decorator(func):
    def wrapper():
        print("=== Before function execution ===")
        func()
        print("=== After function execution ===")
    return wrapper

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

say_hello()

出力:

TEXT
=== Before function execution ===
Hello!
=== After function execution ===

@my_decoratorsay_hello = my_decorator(say_hello) と同等です。シンタックスシュガーによりコードがよりきれいになります。

例:ロギングデコレータ(難易度 ⭐⭐)

PYTHON
def log_call(func):
    """関数呼び出しをログに記録"""
    def wrapper(*args, **kwargs):
        print(f"[LOG] Calling {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} returned {result}")
        return result
    return wrapper

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

@log_call
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(add(3, 5))
print(greet("Zhang San"))
▶ 試してみよう

出力:

TEXT
[LOG] Calling add((3, 5), {})
[LOG] add returned 8
8
[LOG] Calling greet(('Zhang San',), {'greeting': 'Hello'})
[LOG] greet returned Hello, Zhang San!

3. functools.wraps:元の関数情報を保持

デコレータは元の関数を「置き換える」ため、関数名や docstring が失われます。@wraps がこれを修正します:

PYTHON
from functools import wraps

def my_decorator(func):
    @wraps(func)                    # 元の関数情報を保持
    def wrapper(*args, **kwargs):
        """Decorator inner function"""
        print("Before function execution...")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """A simple greeting function"""
    print("Hello!")

print(say_hello.__name__)           # say_hello(@wraps がないと wrapper と表示)
print(say_hello.__doc__)            # A simple greeting function(@wraps がないとデコレータ内部関数と表示)
💡 デコレータを書くときは必ず @wraps を追加しましょう。 これがないと、デコレートされた関数の名前とドキュメントが失われ、デバッグが困難になります。functools から wraps をインポートし、内部のラッパー関数に @wraps(func) を適用します。


4. パラメータ化デコレータ

デコレータ自体にパラメータが必要な場合があります:

PYTHON
from functools import wraps

def repeat(times=2):
    """指定された回数だけ実行を繰り返す"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("Zhang San")

出力:

TEXT
Hello, Zhang San!
Hello, Zhang San!
Hello, Zhang San!

例:タイミングデコレータ(難易度 ⭐⭐⭐)

PYTHON
import time
from functools import wraps

def timer(func):
    """関数の実行時間を計測"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[Timer] {func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    total = 0
    for i in range(1000000):
        total += i
    return total

result = slow_function()
print(f"Result: {result}")
▶ 試してみよう

出力(タイミングはコンピュータによって異なります):

TEXT
[Timer] slow_function took 0.0452 seconds
Result: 499999500000

5. 複数デコレータの組み合わせ

関数には複数のデコレータをスタックできます。実行順序は下から上です:

PYTHON
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def hello():
    return "Hello"

print(hello())              # <b><i>Hello</i></b>
# 実行順序:最初に @italic、次に @bold

# 以下と同等:hello = bold(italic(hello))
💡 複数デコレータは下から上に実行されます。 関数がレイヤーごとに「ラップ」されると考えてください——一番下のデコレータが元の関数に最も近く、最初に実行されます。この順序を理解していれば、複数デコレータによる混乱は起こりません。


よくあるユースケース


❓ よくある質問

Q デコレータで *args**kwargs は必須ですか?
A デコレータがパラメータ数が不明な関数をラップする場合、はい——*args, kwargs ですべての引数を渡します。特定のシグネチャの関数のみをラップする場合は、特定のパラメータ名を指定できますが、汎用性は低くなります。汎用デコレータはほぼ常に *args, kwargs を使用します。
Q デコレータとクロージャの関係は?
A デコレータはクロージャを使って実装されています。クロージャは「外部変数を覚えている関数」です。デコレータはクロージャを活用して、内部のラッパー関数が外部関数から渡された func パラメータにアクセスできるようにします。
Q Python に標準で付属している便利なデコレータには何がありますか?
A @property(属性アクセス)、@staticmethod(静的メソッド)、@classmethod(クラスメソッド)、@functools.lru_cache()(キャッシュ)、@functools.wraps(関数情報保持)、@dataclass(データクラス)。すべて標準ライブラリに含まれています。

📖 まとめ


📝 練習問題

  1. 初級(難易度 ⭐)print_call デコレータを書いてください。関数実行時に「Calling xxx function」を表示します。

  2. 中級(難易度 ⭐⭐)retry(max_attempts=3) デコレータを書いてください。関数が例外を発生させたときに、指定された回数だけ自動的にリトライします。リトライを使い切った場合は、最後の例外を再発生させます。ヒント: ラッパー内で for ループ + try-except を使用。

  3. 上級(難易度 ⭐⭐⭐):パラメータ化デコレータ cache_result(ttl=60) を書いてください。関数の戻り値をキャッシュします。TTL 秒以内に同じパラメータで呼び出された場合はキャッシュされた結果を返し、TTL を超えた場合は値を再計算します。

100%