1. デコレータとは何か?
1‑1. 「装飾(decorate)」のイメージ
| 元の関数 / クラス | デコレータが返すラッパー |
|---|---|
original_function |
wrapper_function (デコレータが生成) |
|
1 2 3 4 5 |
original_function ──► wrapper_function ▲ │ │ ▼ @decorator 呼び出し元 |
- ポイント
- デコレータは「関数(またはクラス)に後から機能を付加」する高階関数です。
- 元の実装はそのままで、呼び出し側のコードは変わりません。
1‑2. なぜデコレータが必要か
- 同じ処理(例: ログ出力・認証チェック)を書き散らすと保守性が低下する。
- デコレータに共通ロジックを集約すれば、一箇所だけ修正すれば全体に反映できる。
2. @ 記法とデコレータの基本構造
2‑1. 関数が関数を返すシンプルな形
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from __future__ import annotations import functools from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) def simple_decorator(func: F) -> F: """引数・戻り値の型情報を保持した最小構造""" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: print(f"[DEBUG] 呼び出し前: {func.__name__}") result = func(*args, **kwargs) print(f"[DEBUG] 呼び出し後: {func.__name__}") return result # `wrapper` の型は `Callable[..., Any]` になるが、元のシグネチャを保持したいので # 型チェックツールに対して無視指示を付ける。 return wrapper # type: ignore[arg-type] |
- ポイント
functools.wrapsがないとwrapper.__name__が'wrapper'になる。*args, **kwargsをそのまま転送すれば、元関数のシグネチャは保たれる。
2‑2. 型情報を正しく伝えるテクニック
2‑2‑1. 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 31 32 33 34 35 36 37 38 39 |
import functools from typing import Callable, TypeVar, ParamSpec, Concatenate, Any P = ParamSpec("P") # 任意の引数リストを表す型変数 R = TypeVar("R") # 戻り値の型変数 class Retry: """ 例外が発生したら指定回数までリトライするクラスデコレータ。 `ParamSpec` と `Concatenate` により、元関数のシグネチャを完全に保持できる。 """ def __init__(self, attempts: int = 3) -> None: self.attempts = attempts def __call__( self, func: Callable[Concatenate[Any, P], R], ) -> Callable[P, R]: """ `func` の最初の引数はインスタンス自身(self ではない)であることを示す `Concatenate[Any, P]` を使う。戻り値は元関数と同じ型 `R`。 """ @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: last_exc: Exception | None = None for i in range(1, self.attempts + 1): try: # `func` はインスタンス自身を受け取らないのでそのまま呼び出す return func(*args, **kwargs) # type: ignore[arg-type] except Exception as exc: # noqa: BLE001 last_exc = exc print(f"[RETRY] {func.__name__} - 試行 {i}/{self.attempts}") # すべて失敗したら最後に捕まえた例外を再送出 raise last_exc # type: ignore[raise-no-exception] return wrapper |
- ポイント
Callable[Concatenate[Any, P], R]は「インスタンス自身(デコレータオブジェクト)と任意の引数P」という意味。wrapperのシグネチャはCallable[P, R]になるので、IDE の補完が正しく機能する。
2‑2‑2. 引数付きデコレータ(ファクトリ)の型定義
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import functools import logging from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) def log_with_level(level: int) -> Callable[[F], F]: """ログレベルを動的に設定できるデコレータファクトリ""" def decorator(func: F) -> F: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: logging.log(level, f"呼び出し: {func.__name__}") return func(*args, **kwargs) return wrapper # type: ignore[arg-type] return decorator |
3. 実務で役立つデコレータ例
3‑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 30 31 32 33 |
import functools import logging from typing import Callable, TypeVar, Any logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", ) F = TypeVar("F", bound=Callable[..., Any]) def log_call(func: F) -> F: """関数呼び出し前後に INFO レベルでログを出す""" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: logging.info(f"関数 {func.__name__} が呼び出されました") result = func(*args, **kwargs) logging.info( f"関数 {func.__name__} が終了しました(戻り値: {result!r})" ) return result return wrapper # type: ignore[arg-type] @log_call def add(a: int, b: int) -> int: """2つの整数を足すだけのシンプル関数""" return a + b # 実行例 add(3, 5) |
期待されるコンソール出力(日時は実行時に変わります)
|
1 2 3 |
2026-04-23 10:12:34,567 - INFO - 関数 add が呼び出されました 2026-04-23 10:12:34,568 - INFO - 関数 add が終了しました(戻り値: 8) |
3‑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 29 |
import functools import time from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) def timed(func: F) -> F: """関数の実行時間(ミリ秒)を標準出力に表示する""" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: start = time.perf_counter() result = func(*args, **kwargs) elapsed_ms = (time.perf_counter() - start) * 1000 print(f"[TIMED] {func.__name__} took {elapsed_ms:.2f} ms") return result return wrapper # type: ignore[arg-type] @timed def heavy_calc(n: int) -> int: total = 0 for i in range(n): total += i * i return total # ベンチマーク例(実行時間は環境に依存) heavy_calc(10_000_000) |
利用シーン
- データパイプラインのボトルネック特定
- Web API のエンドポイント応答時間測定
3‑3. クラスベースデコレータ(リトライ例)※上記 Retry を参照
|
1 2 3 4 5 6 7 8 9 10 |
@Retry(attempts=5) def flaky_api(x: int) -> int: """偶数が来たら失敗する擬似 API""" if x % 2 == 0: raise ValueError("偶数は失敗") return x * 10 # 呼び出し例(3 回目で成功を想定) flaky_api(3) |
ポイントまとめ
| デコレータの形態 | 主なメリット |
|---|---|
| 関数ベース | 実装が簡潔、軽量 |
| 引数付きファクトリ | 動的設定(ログレベル・リトライ回数など)が可能 |
| クラスベース | 状態保持や複数メソッドでの共通ロジックに最適 |
4. デコレータ利用時の落とし穴と対策
4‑1. よくある問題点
| 落とし穴 | 症状例 | 推奨対策 |
|---|---|---|
functools.wraps 未使用 |
デバッグ時に関数名が 'wrapper' になる |
必ず @functools.wraps(func) を付与 |
*args, **kwargs の忘れ |
TypeError: missing … positional argument |
ラップ関数は必ず全引数を転送 |
| 型情報が欠如 | IDE が補完できない、mypy が警告する | ParamSpec/Concatenate で正確に記述 |
| 例外が隠蔽される | スタックトレースがデコレータ内部だけになる | 再スロー時に raise のみ使用(from None を付けない) |
4‑2. デバッグテクニック
-
シグネチャ確認
python
import inspect
print(inspect.signature(wrapped_func))
ラップ後の引数が期待通りかすぐに分かります。 -
ロギングで情報出力
デコレータ内部でlogging.debugを使い、受け取ったargs,kwargs,resultを出力すると原因特定が容易です。 -
IDE の型チェック活用
VSCode + Pylance や PyCharm はデコレータの型ヒントを解析し、未使用引数や戻り値不一致をコンパイル時に警告します。 -
例外スタックトレースの可視化
python
import traceback
try:
risky_func()
except Exception:
traceback.print_exc() # wrapper のフレームも表示され、どこで失敗したかが分かる
4‑3. チェックリスト(実装前に確認)
- [ ]
functools.wrapsが付いている - [ ] ラップ関数は
(*args, **kwargs)をそのまま転送している - [ ] 型ヒントに
ParamSpec/Concatenateなどを用い、元シグネチャが保持されている - [ ] 例外は元のスタックトレースを失わずに再スローしている
5. まとめ
- デコレータは「コードの横断的関心事」を一箇所に集約できる強力な仕組みです。
- 型ヒント(
Callable,TypeVar,ParamSpec,Concatenate)を正しく書けば、IDE の補完や static type checker がフルサポートし、安全かつ可読性の高い実装が可能になります。 - 実務で使うときは必ず
functools.wrapsを入れ、「透明性」(名前・docstring の保持)と 「堅牢さ」(例外伝搬)の両面を担保してください。
次のステップ
1. 本記事のサンプルコードをローカル環境で実行し、挙動を確認。
2. 自プロジェクトに合わせてlog_call・timedを拡張(例: JSON ログ出力や非同期対応)。
3.Retryのようなクラスデコレータで 設定値(リトライ回数、タイムアウト)を外部から注入できるように設計してみましょう。
この記事は Python 3.11 以降を対象としています。型チェックには mypy や pyright を併用すると効果的です。