Contents
1️⃣ デコレータの概要
ポイント
デコレータは「関数やクラスに別の振る舞い(前処理・後処理・状態管理など)を付与する」仕組みです。Python では @ 記号がその糖衣構文として提供され、以下のような変換が自動で行われます。
|
1 2 3 4 5 6 7 8 9 |
def hello(): print("Hello") # デコレータなしで呼び出す hello() # @decorator が書かれているときは次と同等になる hello = decorator(hello) # ← Python が内部で実行してくれる |
背景
Stack Overflow の回答によれば、@ は「decorator を利用できるようにするシンタックスシュガー」であり、本質的には 関数オブジェクトを別の関数(ラッパー)で包んで新しい関数オブジェクトを返す だけです【Stack Overflow】。
2️⃣ デコレータがどのように動くか ― 図解で理解する
2.1 関数デコレータの呼び出しフロー
|
1 2 3 4 5 6 7 8 9 10 11 12 |
+-------------------+ | 呼び出し側 (ユーザ) | +--------+----------+ | hello() ← ユーザが書くコード | +--------v----------+ | wrapper() | ← デコレータが生成したラッパー関数 +--------+----------+ | original() ← 元の関数本体(decorated function) |
2.2 クラスデコレータの呼び出しフロー
|
1 2 3 4 5 6 7 |
@my_class_decorator class Service: ... # 実行時の流れ Service ──► my_class_decorator(Service) ──► 新しいクラスオブジェクト |
ポイント
- デコレータは「元関数/クラス → ラッパー → 元関数/クラス」という 2 段階の呼び出しスタックを作ります。
- ラッパー内部で func(*args, **kwargs) を呼び出すことで、前処理・後処理・例外ハンドリングなど任意のロジック を差し込めます。
3️⃣ 基本的なデコレータ実装パターン
3.1 関数デコレータ(クロージャ版)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from functools import wraps from typing import Callable, TypeVar, ParamSpec, Concatenate P = ParamSpec("P") R = TypeVar("R") def simple_decorator(func: Callable[Concatenate[P], R]) -> Callable[P, R]: @wraps(func) # ← メタデータ(__name__, __doc__)を保持 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: print(f"[DEBUG] 呼び出し前: {func.__name__}") result = func(*args, **kwargs) print(f"[DEBUG] 呼び出し後: {func.__name__}") return result return wrapper |
ParamSpecとConcatenateにより、関数シグネチャを完全に再現でき、IDE の補完や static type checker が正しく動作します(Python 3.12+ 推奨)。
3.2 クラスデコレータ(状態保持型)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from functools import wraps from typing import Any, Callable class CountCalls: """呼び出し回数をカウントするクラスベースのデコレータ""" def __init__(self, func: Callable[..., Any]) -> None: self.func = func self.calls = 0 wraps(func)(self) # メタデータコピー def __call__(self, *args: Any, **kwargs: Any) -> Any: self.calls += 1 print(f"[INFO] {self.func.__name__} が呼ばれた回数: {self.calls}") return self.func(*args, **kwargs) |
使用例
|
1 2 3 4 5 6 7 8 |
@CountCalls def greet(name: str) -> None: print(f"Hello, {name}") greet("Alice") greet("Bob") # => 呼び出し回数が 1, 2 と表示される |
3.3 functools.wraps の重要性
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def logger(level: str = "INFO"): def decorator(func): @wraps(func) # ← 必ず付けること def wrapper(*args, **kwargs): print(f"[{level}] {func.__name__} start") return func(*args, **kwargs) return wrapper return decorator @logger("DEBUG") def add(a: int, b: int) -> int: """2 つの整数を足す""" return a + b |
add.__doc__ は "2 つの整数を足す" のまま保持され、Sphinx 等の自動ドキュメント生成が正しく機能します。
4️⃣ 実務で使えるデコレータ例とフレームワークへの応用
4.1 ログ出力デコレータ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import logging from functools import wraps logging.basicConfig(level=logging.INFO) def log_execution(func): @wraps(func) def wrapper(*args, **kwargs): logging.info("実行開始: %s", func.__name__) result = func(*args, **kwargs) logging.info("実行終了: %s -> %s", func.__name__, result) return result return wrapper |
4.2 実行時間計測デコレータ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import time from functools import wraps def timed(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() value = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"[TIME] {func.__name__}: {elapsed:.4f}s") return value return wrapper |
4.3 シンプル LRU キャッシュデコレータ
|
1 2 3 4 5 6 7 8 9 10 11 |
from functools import lru_cache, wraps def simple_cache(maxsize: int = 128): def decorator(func): cached_func = lru_cache(maxsize=maxsize)(func) @wraps(func) def wrapper(*args, **kwargs): return cached_func(*args, **kwargs) return wrapper return decorator |
4.4 認証チェックデコレータ(Flask 例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from flask import Flask, request, abort from functools import wraps app = Flask(__name__) # ← Flask アプリケーションを明示的に生成 def require_api_key(func): @wraps(func) def wrapper(*args, **kwargs): api_key = request.headers.get("X-API-Key") if api_key != "SECRET_KEY": abort(401) # 認証失敗 → 401 Unauthorized return func(*args, **kwargs) return wrapper @app.route("/secure") # ← Flask のルーティングデコレータを正しく記述 @require_api_key def secure_endpoint(): """API キーが正しければアクセスできるエンドポイント""" return "認証成功" |
ポイント
-app = Flask(__name__)が無いと@app.routeは未定義になるため、サンプルコードでは必ず宣言してください。
- デコレータはrequest.headersからキーを取得し、条件に合わなければabort(401)で例外を送出します。
4.5 Django 用キャッシュデコレータ(型ヒント付き)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from django.core.cache import cache from functools import wraps from typing import Callable, TypeVar, ParamSpec P = ParamSpec("P") R = TypeVar("R") def django_cache(timeout: int = 300): def decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: key = f"{func.__module__}.{func.__qualname__}:{args}{kwargs}" cached = cache.get(key) if cached is not None: return cached result = func(*args, **kwargs) cache.set(key, result, timeout) return result return wrapper return decorator |
5️⃣ デコレータの安全な開発・テスト手法
5.1 メタデータ保持とデバッグ支援
| 手法 | 効果 |
|---|---|
functools.wraps を必ず使用 |
__name__, __doc__, __module__ が失われない。スタックトレースが読みやすくなる |
wrapper.__wrapped__ で元関数にアクセス |
inspect.unwrap() と組み合わせて実装の辿り方を確認できる |
| ロギング・標準出力はテスト時にキャプチャ | capsys, caplog, monkeypatch を利用すれば副作用を検証しやすい |
5.2 可読性・保守性のベストプラクティス
|
1 2 3 4 |
- 命名: 動作が分かる名前 (log_execution, timed, require_api_key) - docstring: ラップ対象関数に対する副作用や前提条件を必ず記載 - 関数分割: 複雑なロジックは内部ヘルパーへ切り出し、デコレータ本体は薄く保つ |
5.3 pytest を用いた実践テスト例
(1) timed デコレータのテスト(出力をキャプチャ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import re import pytest from time import sleep def test_timed_decorator(capsys): @timed def sleepy(sec: float) -> None: """指定秒数だけスリープする""" sleep(sec) # 0.1 秒以上かつ 0.5 秒未満であることを検証 sleepy(0.2) captured = capsys.readouterr().out match = re.search(r"\[TIME\] sleepy: ([0-9.]+)s", captured) assert match is not None, "timed デコレータが出力を行っていない" elapsed = float(match.group(1)) assert 0.1 <= elapsed < 0.5 |
ポイント
timedは例外を投げません。したがってwith pytest.raises(...)を使うのは誤りです。代わりに標準出力やログを捕捉し、実行時間が期待範囲内かどうかで検証します。
(2) ログデコレータのテスト(monkeypatch で logging.info を置き換える)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import logging @pytest.fixture def captured_logs(monkeypatch): logs = [] def fake_info(msg, *args, **kwargs): logs.append(msg % args) monkeypatch.setattr(logging, "info", fake_info) return logs def test_log_execution(captured_logs): @log_execution def add(a, b): """単純な加算""" return a + b assert add(2, 3) == 5 # 開始ログと終了ログの両方が記録されていることを確認 assert any("実行開始: add" in m for m in captured_logs) assert any("実行終了: add -> 5" in m for m in captured_logs) |
5.4 テスト時に気をつけるべき副作用
| 副作用 | 推奨テクニック |
|---|---|
標準出力 (print) |
capsys / capfd |
ログ出力 (logging) |
monkeypatch でハンドラ差し替え、または caplog |
| グローバル状態(カウンタ等) | テストごとにインスタンスを再生成、もしくは fixture のスコープを function に限定 |
6️⃣ 記事のまとめ
- デコレータは
@記号で関数やクラスをラップし、前処理・後処理・状態管理 といった横断的な機能を簡潔に付加できる強力な構文です。 - 図解とコード例 を通じて「呼び出しフロー → ラッパー生成 → 元関数実行」の流れが把握できます。
functools.wrapsと Python 3.12 の型ヒント (ParamSpec,Concatenate) を併用すれば、メタデータ保持と型安全性の両立が可能です。- 実務で頻出する ログ出力・実行時間計測・キャッシュ・認証チェック は数行のコードで実装でき、Flask/Django といったフレームワークでも同様に活用できます(例:
require_api_key)。 - テストは副作用を捕捉することが鍵。
pytestの fixture・monkeypatch・capsys を駆使し、デコレータの期待動作(出力、ログ、状態変化)を明示的に検証しましょう。
デコレータは「コードの再利用性」と「横断的関心事の分離」を同時に実現できる、Python 開発者なら必ず身につけておきたいテクニックです。この記事で紹介したベストプラクティスを踏まえて、プロジェクトに合わせたカスタムデコレータを安全かつ効率的に導入してください。