Contents
1. デコレータの基本形と動作イメージ
1‑1. 関数デコレータ(最もシンプルな例)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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"[timer] {func.__qualname__} took {elapsed:.4f}s") return result return wrapper @timer def heavy_compute(x: int) -> int: """サンプル処理:単純なループ""" total = 0 for i in range(x): total += i * i return total # 実行例 heavy_compute(10_000) # => [timer] heavy_compute took 0.0231s |
1‑2. クラスデコレータで状態を保持
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import functools class CallCounter: """呼び出し回数をカウントし、属性として公開するクラスデコレータ""" def __init__(self, func): functools.update_wrapper(self, func) # メタ情報コピー self._func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"[CallCounter] {self.__qualname__} called {self.count} time(s)") return self._func(*args, **kwargs) @CallCounter def greet(name: str) -> None: """挨拶を出力する関数""" print(f"Hello, {name}!") greet("Alice") # => [CallCounter] greet called 1 time(s) # => Hello, Alice! |
ポイント
| パターン | メリット |
|---|---|
関数デコレータ + @functools.wraps |
メタ情報(__name__, __doc__, __module__, __qualname__)が保持され、IDE の補完が正しく動作 |
| クラスデコレータ | インスタンス属性で永続的な状態を持たせられる。複数関数に同じロジックを適用したいときに便利 |
2. functools.wraps が必須な理由
デコレータは内部で新しい関数オブジェクト(wrapper)を生成します。属性転送を行わないと、次のような問題が起きます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def bad_decorator(func): def wrapper(*a, **k): # @wraps が無い return func(*a, **k) return wrapper @bad_decorator def sample(a: int, b: str = "x") -> bool: """サンプル関数""" return True print(sample.__name__) # → wrapper (本来は sample) import inspect print(inspect.signature(sample))# → () -> Any (情報が失われた) |
@functools.wraps(または functools.update_wrapper) を付けるだけで、上記の「名前・ドキュメント・シグネチャ」すべてが元関数からコピーされます。
3. 型安全な汎用デコレータ ― ParamSpec の活用
Python 3.10 以降で利用可能な typing.ParamSpec は「任意の引数列」を型変数として扱えるため、デコレータが受け取るシグネチャを正確に表現できます。ここでは Concatenate を省き、実務で十分に活用できる形だけ示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# Python 3.10+ from __future__ import annotations import functools import typing as t P = t.ParamSpec('P') # 任意の位置・キーワード引数列 R = t.TypeVar('R') # 戻り値型 def log_calls(func: t.Callable[P, R]) -> t.Callable[P, R]: """呼び出し情報を標準出力にログする型安全デコレータ""" @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: arg_repr = ', '.join( [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()] ) print(f"[log_calls] {func.__qualname__}({arg_repr})") return func(*args, **kwargs) # 型はそのまま R と推論される return wrapper @log_calls def compute(x: int, y: int = 0) -> int: """2数の加算を行うサンプル関数""" return x + y compute(5, y=7) # => [log_calls] compute(5, y=7) |
効果
- IDE が
computeのシグネチャ(x: int, y: int = 0) -> intを正しく補完 - デコレータ内部で型情報が失われないので、他のデコレータと組み合わせても安全
4. 実務でよく使う応用例
4‑1. ロギングデコレータ(標準 logging を利用)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import logging, functools logging.basicConfig(level=logging.INFO) def simple_logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): logging.info( "Calling %s with args=%s kwargs=%s", func.__qualname__, args, kwargs ) return func(*args, **kwargs) return wrapper @simple_logger def add(a: int, b: int) -> int: return a + b add(3, 5) # INFO:root:Calling add with args=(3, 5) kwargs={} |
ポイント
- logging の設定だけで出力先・フォーマットを一元管理できる。
- デコレータ自体は「前処理」しか持たないので、テストがしやすい。
4‑2. 認証チェックデコレータ(ParamSpec と組み合わせ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
from __future__ import annotations import functools, typing as t P = t.ParamSpec('P') R = t.TypeVar('R') class User: def __init__(self, name: str, roles: list[str]): self.name = name self.roles = roles def require_role(role: str): """指定ロールが無いと例外を送出するデコレータ""" def decorator(func: t.Callable[P, R]) -> t.Callable[P, R]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: user = kwargs.get('user') if not user or role not in user.roles: raise PermissionError(f"Role '{role}' required") return func(*args, **kwargs) return wrapper return decorator @require_role('admin') def delete_record(record_id: int, *, user: User) -> None: print(f"Record {record_id} deleted by {user.name}") admin = User("Alice", ["admin", "editor"]) delete_record(42, user=admin) # 正常に実行 guest = User("Bob", ["viewer"]) # delete_record(43, user=guest) # PermissionError が送出される |
ポイント
kwargs.get('user')のように 明示的な依存注入 にすれば、テストコードでモックユーザーを渡すだけでシナリオ検証が容易。ParamSpecがあるため、デコレータ適用後も元関数のシグネチャはそのまま保持される。
4‑3. 安全なキャッシュデコレータ(ハッシュ不可能引数に対応)
標準ライブラリの functools.lru_cache は内部で _make_key を使い、*args と **kwargs のハッシュ化を安全に行っています。ここでは同様のロジックを自前実装し、非ハッシュ可能なオブジェクト(list, dict など)でも例外が出ない ようにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
from __future__ import annotations import functools, typing as t, hashlib, pickle P = t.ParamSpec('P') R = t.TypeVar('R') def _hash_key(args: tuple[t.Any, ...], kwargs: dict[str, t.Any]) -> str: """ args/kwargs をシリアライズし SHA256 ハッシュ文字列に変換する。 pickle は非ハッシュ可能オブジェクトも扱えるが、信頼できるデータだけに使用してください。 """ data = (args, tuple(sorted(kwargs.items()))) return hashlib.sha256(pickle.dumps(data)).hexdigest() def simple_cache(func: t.Callable[P, R]) -> t.Callable[P, R]: """ParamSpec 対応・ハッシュ安全な軽量キャッシュ""" cache: dict[str, R] = {} @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: key = _hash_key(args, kwargs) if key in cache: print("[cache] hit") return cache[key] result = func(*args, **kwargs) cache[key] = result print("[cache] miss") return result return wrapper @simple_cache def heavy_calc(x: int, y: list[int]) -> int: """リストを受け取るがキャッシュ可能な例""" import time; time.sleep(0.2) # 重い処理のシミュレーション return x * sum(y) print(heavy_calc(3, [1, 2, 3])) # → miss print(heavy_calc(3, [1, 2, 3])) # → hit |
解説
pickle.dumpsはリストや辞書などハッシュ不可能なオブジェクトもシリアライズできるため、キー生成でTypeErrorが起きない。- ハッシュ化に SHA256 を使うので、キーサイズは一定(64 文字)になりメモリ使用量が予測しやすい。
- 本実装は 純粋関数 前提です。副作用がある関数には適用しないでください。
5. デコレータの適用順序とスタック効果 – 正しい理解
5‑1. 「装飾時」と「呼び出し時」の2段階プロセス
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import functools def deco_a(func): @functools.wraps(func) def wrapper(*a, **k): print(">>> A before") r = func(*a, **k) # ← 次のデコレータ(または本体)を呼び出す print("<<< A after") return r return wrapper def deco_b(func): @functools.wraps(func) def wrapper(*a, **k): print(">>> B before") r = func(*a, **k) # ← 本体へ print("<<< B after") return r return wrapper @deco_a # ★装飾時に最初に評価される(外側) @deco_b # ★装飾時に次に評価される(内側) def target(): print("--- core ---") target() |
実行結果
|
1 2 3 4 5 6 |
>>> A before # deco_a の wrapper が最初に呼ばれる >>> B before # その内部で deco_b の wrapper が続く --- core --- <<< B after <<< A after |
ポイント
| 観点 | 正しい説明 |
|---|---|
装飾時(@ 行が実行されるタイミング) |
Python は 下から上へ デコレータを適用する。すなわち、最も近い @deco_b が先に呼び出され、戻り値(ラップされた関数)が次のデコレータ @deco_a に渡される |
| 呼び出し時 | ラップされた関数は「外側 → 内側」の順で実行される。上に書いた deco_a が最初に前処理を行い、内部で deco_b(内側)が続く |
つまり 「装飾時は下から上」、「呼び出し時は上から下」 が正しい順序です。
この逆転を意識せずにデコレータを組み合わせると、たとえば「認証 → ロギング」の期待が ロギング → 認証 になり、セキュリティ上のバグにつながります。
5‑2. 順序が重要になる典型シナリオ
| シナリオ | 正しい装飾順(上から下) | 実行時の流れ |
|---|---|---|
| 認証 → ロギング | @logger@require_role('admin') |
1. require_role が先にチェック、2. 合格したらロガーが記録 |
| トランザクション管理 → キャッシュ | @cache@transactional |
1. トランザクション開始・終了を保証、2. 結果はキャッシュに保存 |
6. まとめ & ベストプラクティスチェックリスト
| ✅ チェック項目 | 推奨実装例 |
|---|---|
@functools.wraps を必ず付与 |
@functools.wraps(func) |
型安全は ParamSpec で確保 |
def deco(func: Callable[P, R]) -> Callable[P, R]: … |
| キャッシュキーはハッシュ不可能に備える | _hash_key(pickle + SHA256) |
| デコレータの適用順序を意識 | コメントで「外側 / 内側」マーク、テストで実行順序検証 |
| 副作用がある関数はキャッシュ対象にしない | ドキュメントで明示 |
| ログ・認証は共通インタフェースを持たせる | *args, **kwargs と ParamSpec の併用 |
参考コード(すべてまとめ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# ------------------------------------------------- # utils.py – 共通ユーティリティ # ------------------------------------------------- from __future__ import annotations import functools, hashlib, pickle, logging, typing as t P = t.ParamSpec('P') R = t.TypeVar('R') def _hash_key(args: tuple[t.Any, ...], kwargs: dict[str, t.Any]) -> str: data = (args, tuple(sorted(kwargs.items()))) return hashlib.sha256(pickle.dumps(data)).hexdigest() # ------------------------------------------------- # decorators.py – 本稿で紹介したデコレータ群 # ------------------------------------------------- def timer(func): @functools.wraps(func) def wrapper(*a, **k): import time s = time.perf_counter() r = func(*a, **k) print(f"[timer] {func.__qualname__} took {time.perf_counter() - s:.4f}s") return r return wrapper def simple_logger(func): @functools.wraps(func) def wrapper(*a, **k): logging.info("Calling %s args=%s kwargs=%s", func.__qualname__, a, k) return func(*a, **k) return wrapper def require_role(role: str): def deco(func: t.Callable[P, R]) -> t.Callable[P, R]: @functools.wraps(func) def wrapper(*a: P.args, **k: P.kwargs) -> R: user = k.get('user') if not user or role not in getattr(user, 'roles', []): raise PermissionError(f"Role '{role}' required") return func(*a, **k) return wrapper return deco def simple_cache(func: t.Callable[P, R]) -> t.Callable[P, R]: cache: dict[str, R] = {} @functools.wraps(func) def wrapper(*a: P.args, **k: P.kwargs) -> R: key = _hash_key(a, k) if key in cache: print("[cache] hit") return cache[key] res = func(*a, **k) cache[key] = res print("[cache] miss") return res return wrapper # ------------------------------------------------- # usage_example.py – 実際の利用例 # ------------------------------------------------- import logging from decorators import timer, simple_logger, require_role, simple_cache logging.basicConfig(level=logging.INFO) @timer @simple_logger def fibonacci(n: int) -> int: if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(10)) # 認証+キャッシュの組み合わせ例 class User: def __init__(self, name, roles): self.name = name self.roles = roles @require_role('admin') @simple_cache def expensive_query(q: str, *, user: User) -> list[int]: import random, time time.sleep(0.3) return [random.randint(0, 100) for _ in range(5)] admin = User('Alice', ['admin']) print(expensive_query("SELECT *", user=admin)) # miss print(expensive_query("SELECT *", user=admin)) # hit |
以上が、指摘事項をすべて反映した改訂版記事です。
デコレータの基本から型安全化、実務で直面しやすい課題まで網羅的に解説しましたので、ぜひプロジェクトのコードベースへ取り入れてみてください。