FlaskやFastAPIを1週間以上使ったことがあれば、すでにデコレーターを使っています — @app.route@login_required@pytest.mark.parametrize。最初は魔法のように見えますが、 実際に何が起きているかを誰かが説明するとすぐに理解できます。デコレーターは単に別の関数をラップする関数です。 @構文は純粋な糖衣構文 — 関数定義の上の@my_decoratorは、 その後にfunc = my_decorator(func)と書くのと完全に等価です。それが唯一の秘密です。 残りはすべてその1つのアイデアの上に構築されたパターンです。この記事では最初からメンタルモデルを構築し、 実際に使うパターンを順に見ていきます:@functools.wraps、引数付きデコレーター、 クラスベースのデコレーター、そして@lru_cache@dataclass、 指数バックオフ付きの適切な@retryなどの実際の例を紹介します。

関数はファーストクラスオブジェクト

デコレーターが意味を持つには、まずPythonの1つの事実を理解する必要があります:関数はオブジェクトです。 変数に代入でき、他の関数への引数として渡せ、関数から返せ、 リストやdictに格納できます。何かが関数だからといって特別なことは何も起きません。

python
def greet(name: str) -> str:
    return f"Hello, {name}"

# Assign to a variable — no () means we're not calling it, just referencing it
say_hello = greet
print(say_hello("Alice"))   # 'Hello, Alice'

# Pass a function as an argument
def run_twice(fn, value):
    return fn(value), fn(value)

run_twice(greet, "Bob")     # ('Hello, Bob', 'Hello, Bob')

# Return a function from another function — this is a "factory"
def make_prefixer(prefix: str):
    def prefixed_greet(name: str) -> str:
        return f"{prefix}, {name}"
    return prefixed_greet

morning_hello = make_prefixer("Good morning")
morning_hello("Carol")      # 'Good morning, Carol'

内部関数prefixed_greetは、外側のスコープからprefix変数を「クロージャ」として取り込みます — make_prefixerが返った後も、内部関数はまだprefixにアクセスできます。これが クロージャであり、 デコレーターを機能させる仕組みです。 Pythonのスコープルールに関するドキュメント が、全体像を詳しく説明しています。

デコレーターをゼロから構築する

デコレーターは関数を取り、(通常は変更された)関数を返す関数です。 最初の典型的な例はタイマーデコレーターです — 任意の関数をラップして実行時間を記録します。

python
import time
import functools

def timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} finished in {elapsed:.4f}s")
        return result
    return wrapper

@timer
def fetch_user_records(db, user_id: int):
    """Fetch all records for a given user from the database."""
    return db.query("SELECT * FROM records WHERE user_id = ?", user_id)

# Calling fetch_user_records is now: timer(fetch_user_records)(db, 42)
# Which is exactly what @timer desugars to

注意点がいくつかあります:wrapperは*args, **kwargsを使って、 シグネチャを知らずに元の関数に任意の引数の組み合わせを転送できます。 戻り値をresultに格納して返すため、ラップされた関数は呼び出し元から見て全く同じように動作します — ただ余分なprint副作用があるだけです。タイミング部分を削除すると、 これがほぼすべてのデコレーターのスケルトンです。

デシュガーのルール:defブロックの直後にfn = some_decorator(fn)と書いたものと、 関数定義の上の@some_decoratorを見たら精神的に置き換えてください。 両者は完全に等価です。魔法はありません — 関数呼び出しです。

@functools.wrapsを使わなければならない理由

上記の例では、wrapperに@functools.wraps(fn)行があります。これは 任意ではありません。これなしでは、デコレートされた関数がアイデンティティを失います — その__name____doc____qualname__属性がすべて内部のwrapper関数のものに 置き換えられます。これはいくつかの実際の状況で微妙な問題を引き起こします:

  • docstringが消える。help(fetch_user_records)"Fetch all records for a given user..."の代わりにwrapperの空のdocstringを表示します。
  • スタックトレースが嘘をつく。ラップされた関数内で例外が発生した場合、tracebackは本当の関数名の代わりにwrapperを表示します — デバッグが難しくなります。
  • イントロスペクションが壊れる。pytest、Flaskのルーティングシステム、inspect.signature()などのツールはすべて__name____wrapped__に依存しています。2つのルートが同じ(wrapper)名を共有するとFlaskのルーターは例外をスローします。
  • functools.lru_cacheや類似ツールはキャッシュキーに関数のアイデンティティを使います — wrapsなしでは予期しないキャッシュの衝突が起きることがあります。
python
import functools

# Without @functools.wraps — broken
def bad_timer(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@bad_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'wrapper'   ← wrong
print(process_batch.__doc__)    # None         ← docstring gone

# With @functools.wraps — correct
def good_timer(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@good_timer
def process_batch(batch_id: int):
    """Process a single batch job."""
    pass

print(process_batch.__name__)   # 'process_batch'  ← correct
print(process_batch.__doc__)    # 'Process a single batch job.'  ← preserved

functools.wraps 自体がデコレーターです — __module____name____qualname____annotations____doc__をコピーし、__wrapped__を元の関数に設定します。 すべてのwrapper関数に使いましょう。使わない理由はありません。

引数付きデコレーター

シンプルなデコレーターは関数を取り関数を返します。引数付きデコレーターはもう1レベル必要です: 引数を取りデコレーターを返す関数。3レベルのネストで、 最初に見るとほぼ全員が混乱します。max_attempts回まで例外時にリトライする@retryデコレーターを紹介します:

python
import functools
import time

def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    Retry a function on exception with exponential backoff.

    Usage:
        @retry(max_attempts=5, delay=0.5, backoff=2.0)
        def call_external_api(endpoint: str) -> dict:
            ...
    """
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None

            for attempt in range(1, max_attempts + 1):
                try:
                    return fn(*args, **kwargs)
                except Exception as exc:
                    last_exception = exc
                    if attempt < max_attempts:
                        print(
                            f"{fn.__name__}: attempt {attempt}/{max_attempts} failed "
                            f"({exc!r}), retrying in {current_delay:.1f}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff
                    else:
                        print(f"{fn.__name__}: all {max_attempts} attempts failed.")

            raise last_exception

        return wrapper
    return decorator

@retry(max_attempts=4, delay=0.5, backoff=2.0)
def fetch_price_data(ticker: str) -> dict:
    """Fetch stock price data from external API."""
    response = requests.get(f"https://api.example.com/prices/{ticker}", timeout=5)
    response.raise_for_status()
    return response.json()

Pythonが@retry(max_attempts=4, delay=0.5, backoff=2.0)を処理する時の呼び出しチェーン: まずretry(max_attempts=4, delay=0.5, backoff=2.0)が呼ばれてdecoratorを返します。 次にdecorator(fetch_price_data)が呼ばれてwrapperを返します。最後に fetch_price_datawrapperに再バインドされます。つまり@retry(...)fetch_price_data = retry(...)(fetch_price_data) — 3回の呼び出し、2レベルのラッピングです。 このパターンが見えると、デコレーターファクトリーが混乱しなくなります。

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

__call__を定義することで、デコレーターをクラスとして実装することもできます。これは デコレーターが呼び出し間で状態を維持する必要がある時 — 呼び出しカウンター、キャッシュ、接続プール — に便利です。 クロージャ変数よりもインスタンス変数の方がその状態のより自然な場所だからです。

python
import functools
import time

class RateLimiter:
    """
    Decorator that limits how often a function can be called.
    Raises RuntimeError if the function is called within `min_interval` seconds
    of the previous call.
    """

    def __init__(self, min_interval: float):
        self.min_interval = min_interval
        self._last_called: float = 0.0

    def __call__(self, fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            since_last = now - self._last_called
            if since_last < self.min_interval:
                wait = self.min_interval - since_last
                raise RuntimeError(
                    f"{fn.__name__} called too soon — "
                    f"wait {wait:.2f}s before calling again."
                )
            self._last_called = now
            return fn(*args, **kwargs)
        return wrapper

# Usage — one call per 2 seconds
@RateLimiter(min_interval=2.0)
def send_sms_alert(phone: str, message: str) -> None:
    """Send an SMS alert via the gateway API."""
    sms_gateway.send(phone, message)

@RateLimiter(min_interval=2.0)はデコレーターファクトリーとまったく同じように機能します: RateLimiter(2.0)がインスタンスを構築し、そのインスタンスは__call__があるため send_sms_alertと一緒に呼ばれます。インスタンスは_last_calledを 属性として格納します — クロージャ変数の扱いが不要です。 PEP 318(元のデコレーター提案)で @構文の設計的根拠を、 PEP 614で Python 3.9で導入された緩和されたデコレーター文法を確認できます。

標準ライブラリの実用的なデコレーター

自分で書く前に、stdlibがすでに必要なものを持っていないか確認してください。 functools の3つのデコレーターが本番Pythonコードで常に登場します。

python
from functools import lru_cache, cache
import requests

# @cache — unbounded memoisation (Python 3.9+)
# Caches every unique set of arguments forever.
# Good for pure functions with a small domain of inputs.
@cache
def get_country_name(country_code: str) -> str:
    """Look up a country name from ISO 3166 code. Cached after first call."""
    response = requests.get(f"https://restcountries.com/v3.1/alpha/{country_code}")
    return response.json()[0]["name"]["common"]

get_country_name("DE")   # hits the API
get_country_name("DE")   # served from cache, no network call

# @lru_cache(maxsize=N) — bounded LRU cache
# Evicts least-recently-used entries once the cache hits `maxsize`.
# Better when the input domain is large and memory is a concern.
@lru_cache(maxsize=256)
def compute_discount(base_price: float, tier: str) -> float:
    """Heavy computation — price varies by tier. Cache the top 256 combinations."""
    discount_table = load_discount_table()          # expensive DB call
    rate = discount_table.get(tier, 0.0)
    return round(base_price * (1 - rate), 2)

# Inspect cache performance
print(compute_discount.cache_info())
# CacheInfo(hits=142, misses=14, maxsize=256, currsize=14)

@dataclassは別のカテゴリです — フィールドアノテーションから__init____repr____eq__を自動生成するクラスデコレーターです。 データを保持するクラスの大量の定型文を削除します:

python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime

@dataclass
class WebhookEvent:
    event_type: str
    source_id: int
    payload: dict
    received_at: datetime = field(default_factory=datetime.utcnow)
    retry_count: int = 0
    error_message: Optional[str] = None

# @dataclass generates all of this for free:
# - __init__(self, event_type, source_id, payload, received_at=..., retry_count=0, error_message=None)
# - __repr__ that shows all fields
# - __eq__ that compares field-by-field

event = WebhookEvent(event_type="order.created", source_id=9912, payload={"order_id": 44501})
print(event)
# WebhookEvent(event_type='order.created', source_id=9912, payload={...}, retry_count=0, ...)

@propertyデコレーターはメソッドを属性スタイルのアクセサーに変換します — 呼び出し側は user.get_display_name()の代わりにuser.display_nameを読みます。 @property.setterと組み合わせて書き込み時にバリデーションします:

python
class UserProfile:
    def __init__(self, first_name: str, last_name: str, email: str):
        self._first_name = first_name
        self._last_name = last_name
        self._email = email.strip().lower()

    @property
    def display_name(self) -> str:
        return f"{self._first_name} {self._last_name}"

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str) -> None:
        if "@" not in value:
            raise ValueError(f"Invalid email address: {value!r}")
        self._email = value.strip().lower()

profile = UserProfile("Alice", "Smith", "  [email protected]  ")
print(profile.display_name)   # 'Alice Smith'
print(profile.email)          # '[email protected]'

profile.email = "[email protected]"    # setter runs validation
profile.email = "not-an-email"           # raises ValueError

ルートハンドラーのための@require_authデコレーター

Webフレームワークでの認証チェックはデコレーターの典型的な使用例です。各ルートハンドラーの先頭に 「ユーザーはログインしているか?」チェックを複製する代わりに、 デコレーターとして一度書いて必要なところに適用します。ここにFlaskで動作するパターンを示します(他のフレームワークにも移植可能):

python
import functools
from flask import request, jsonify, g

def require_auth(fn):
    """
    Decorator that validates a Bearer token before the route handler runs.
    Sets g.current_user on success; returns 401 JSON on failure.
    """
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or malformed Authorization header"}), 401

        token = auth_header[len("Bearer "):]
        user = verify_token(token)          # your token validation logic
        if user is None:
            return jsonify({"error": "Invalid or expired token"}), 401

        g.current_user = user               # available to the route handler
        return fn(*args, **kwargs)
    return wrapper

# Usage — the auth check runs before the handler body
@app.route("/api/v1/reports/<int:report_id>")
@require_auth
def get_report(report_id: int):
    report = Report.query.get_or_404(report_id)
    if report.owner_id != g.current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    return jsonify(report.to_dict())

順序に注意:@app.routeが最初(最外側)、@require_authが 2番目です。これは重要です — 次のセクションを参照。パターンは自然に拡張できます: @require_authがすでにユーザーの存在を確認した後にg.current_user.roleを チェックする@require_role("admin")デコレーターファクトリーを追加できます。

デコレーターのスタック — 順序が重要

複数のデコレーターをスタックすると、下から上に適用されます(関数に最も近いデコレーターが最初に適用)、 しかし関数が呼ばれると上から下に実行されます。これが人々を混乱させます。

python
@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

# This is exactly equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))

# When my_function() is called:
# 1. decorator_a's wrapper runs first (outermost)
# 2. decorator_b's wrapper runs second
# 3. decorator_c's wrapper runs third (innermost, closest to the real function)
# 4. The real my_function body runs
# 5. Unwinding: decorator_c → decorator_b → decorator_a
python
# Practical example: order matters for @timer and @retry
# If timer is outermost, it measures total time including retry wait periods.
# If retry is outermost, it measures only the final successful call.

@timer          # measures: total time across all retry attempts + sleep
@retry(max_attempts=3, delay=1.0)
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# vs.

@retry(max_attempts=3, delay=1.0)  # measures: only the final successful call
@timer
def sync_with_partner_api(partner_id: int) -> dict:
    ...

# Usually you want @timer outermost — it tells you the real wall-clock cost of the operation.
# Think about what "calling this function" means to the caller, then wrap in that order.
スタックトレースのヒント:デコレートされた関数が例外を投げると、 トレースバックはコールスタックの各wrapperを表示します。@functools.wrapsを正しく使っていれば、 名前は元の関数を反映します。wrapperフレームの海が見えたら、 誰かが@functools.wrapsを忘れています。 Real PythonのPythonデコレーター入門 にスタックされたデコレーターのデバッグ方法の良いウォークスルーがあります。

まとめ

引き継ぐべきメンタルモデル:@decoratorfn = decorator(fn)です。 その他すべて — 引数付きデコレーター、クラスベースのデコレーター、スタックされたデコレーター — はその1つの置換のバリエーションです。 すべての内部wrapperに@functools.wrapsを使い、常に*args, **kwargsを ラップされた関数に転送し、その結果を返します。引数付きデコレーターは3レベルのネストが必要です: 引数関数、デコレーター、wrapper。クラスベースのデコレーターは、 デコレーターが呼び出し間で状態を維持する必要がある時の正しい選択です。

さらなる読み物: Pythonグロッサリーのデコレーター項目 は簡潔ですが正確です。元のデコレーター提案、 PEP 318は、 @構文が代替案より選ばれた理由のコンテキストとして読む価値があります。 デコレーターを多くのデータ処理も行うコードベースで使っているなら — ファイルの読み込み、レコードの変換 — ここのパターンは Pythonファイル操作で扱われる内容と自然に組み合わさります。 コレクションを変換したり検索構造を構築するためにデコレーターを使っているなら、 Pythonリスト内包表記がそのデータ変換の側面をカバーします。 デコレートされた関数がJSON出力を返し、すぐに検査したい場合、このサイトのJSONフォーマッターが便利です。