C
Python/中級/Lesson 12

デコレータ (Decorator)

1時間·theory
このチャプター
4/8
Python

デコレータ (Decorator)

🎯 このlessonを読み終えたら

このlessonを読み終えると、以下の3つを自信を持って実践できます。

  • ✅ 関数が関数を返す高階関数の原理
  • @decorator構文とfunctools.wrapsが必要な理由
  • @lru_cache@property@staticmethodの活用

学習目標をチェックリストとして、すべて答えられるようになったらlessonを閉じてください。

デコレータ — コード+実行結果

@decorator = 関数をラップして機能を追加。ロギング・キャッシング・認証といった共通処理を分離する。

1. 最もシンプルなデコレータ

python
def ログ(元の関数):
    def ラップ済み(*args, **kwargs):
        print(f"呼び出し: {元の関数.__name__}({args})")
        結果 = 元の関数(*args, **kwargs)
        print(f"結果: {結果}")
        return 結果
    return ラップ済み

@ログ
def 足し算(a, b):
    return a + b

足し算(3, 5)
# 出力:
# 呼び出し: 足し算((3, 5))
# 結果: 8

@ログ足し算 = ログ(足し算) と同じ意味です。

2. 実行時間の計測

python
import time

def 時間測定(f):
    def ラップ済み(*args, **kwargs):
        開始 = time.time()
        結果 = f(*args, **kwargs)
        経過 = time.time() - 開始
        print(f"{f.__name__}: {経過:.3f}秒")
        return 結果
    return ラップ済み

@時間測定
def 重い処理():
    time.sleep(1)
    return "完了"

重い処理()      # 重い処理: 1.001秒

3. 引数を受け取るデコレータ

python
def 繰り返し(回数):
    def デコ(f):
        def ラップ済み(*args, **kwargs):
            for _ in range(回数):
                結果 = f(*args, **kwargs)
            return 結果
        return ラップ済み
    return デコ

@繰り返し(3)
def 挨拶():
    print("こんにちは!")

挨拶()
# こんにちは!
# こんにちは!
# こんにちは!

4. よく使う組み込みデコレータ

python
from functools import lru_cache

@lru_cache(maxsize=100)         # 結果キャッシュ — 同じ引数なら即時返却
def フィボナッチ(n):
    return n if n < 2 else フィボナッチ(n-1) + フィボナッチ(n-2)

フィボナッチ(50)                    # キャッシュのおかげで即時 (なければ千億回呼び出し)

@property(getter)、@staticmethod@classmethod もクラスでよく使われます。

一行まとめ

パターンコード
シンプル@デコレータ
引数あり@デコレータ(引数)
キャッシュ@lru_cache
アクセス@property
💻 悪い例 — functools.wrapsなしで実装
# wrapsなしで作成したデコレータ — デバッグが難しくなる
def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        print(f"実行時間: {time.time() - start:.4f}s")
        return result
    return wrapper  # wrapper関数名が元の関数を上書き

@timer
def calculate(n):
    """nの二乗を計算します"""
    return n ** 2

print(calculate.__name__)  # 'wrapper' — 元の名前が失われる!
print(calculate.__doc__)   # None — ドキュメント文字列が失われる!
💻 良い例 — functools.wraps+引数ありのデコレータ
import functools
import time
import logging

# 基本デコレータ — functools.wraps 必須
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

# 引数付きデコレータ — 3重ネスト
def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"再試行 {attempt}/{max_attempts}: {e}")
        return wrapper
    return decorator

# 実践: FastAPIスタイル認証デコレータ
def require_auth(func):
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.get('user'):
            raise PermissionError("ログインが必要です")
        return func(request, *args, **kwargs)
    return wrapper

@timer
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    """外部APIデータ照会"""
    import urllib.request
    with urllib.request.urlopen(url) as response:
        import json
        return json.loads(response.read())

print(fetch_data.__name__)  # 'fetch_data' — 元の名前を保持
print(fetch_data.__doc__)   # '外部APIデータ照会' — ドキュメント文字列を保持
💻 実践例 — クラスベースのデコレータ(キャッシュ)
import functools
from typing import Callable, Any

# クラスベースデコレータ — 状態管理に有利
class Cache:
    """シンプルなメモ化キャッシュデコレータ"""
    
    def __init__(self, max_size: int = 128):
        self.max_size = max_size
        self._cache: dict = {}
    
    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            if key not in self._cache:
                if len(self._cache) >= self.max_size:
                    # LRU簡易実装: 最も古い項目を削除
                    oldest = next(iter(self._cache))
                    del self._cache[oldest]
                self._cache[key] = func(*args, **kwargs)
            return self._cache[key]
        wrapper.cache_clear = lambda: self._cache.clear()
        wrapper.cache_info = lambda: {'size': len(self._cache), 'max': self.max_size}
        return wrapper

@Cache(max_size=64)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))              # 高速に計算
print(fibonacci.cache_info())     # {'size': 51, 'max': 64}
fibonacci.cache_clear()           # キャッシュ初期化

# 参考: 標準ライブラリの functools.lru_cacheも同じ役割
from functools import lru_cache

@lru_cache(max_size=128)
def factorial(n: int) -> int:
    return 1 if n == 0 else n * factorial(n - 1)

🐍 実行してみよう — デコレータ (Decorator)

上記の概念を実際にコードで動かしてみてください。値を変えながら動作を直接確認するのが最も速い学習方法です。
✏️ Python 코드
📟 コンソール出力
▶ 実行ボタンを押してください
🐍 Pyodideで実際のPythonを実行 — 初回は読み込みに3〜5秒

🤖 AIへの依頼例

このlessonの概念を知っていると、AIに具体的な指示が出せます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「この関数にfunctools.lru_cacheデコレータを適用して」
  • 「functools.wrapsを使ってタイミング・ロギングデコレータを作って」

これがトークンを減らす理由

概念を知らないと、AIの回答を受け取っても「それは何ですか?」と再度質問しなければなりません。その「再質問」がトークンを消費します。概念を一度習得しておけば、会話が一度で完結します

先に読むとよい概念: ジェネレーター
デコレーター - Python