Contents
1. デコレータとは何か
1-1. 本質
デコレータは 「関数(またはメソッド)を受け取り、同じ呼び出し側のインターフェースを保ったまま機能を拡張した新しい関数」を返す高階関数 です。
@decorator_nameはその手続きを シンタックスシュガー として提供し、Python コンパイラが自動的に
|
1 2 3 4 |
@dec def f(...): ... |
を
|
1 2 3 4 |
def f(...): ... f = dec(f) # ← デコレータが適用された結果の代入 |
という形に変換します。
1-2. なぜ使えるのか
Python の関数は 第一級オブジェクト(変数に代入でき、引数や戻り値にできる)であるため、以下が自然に書けます。
|
1 2 3 4 |
def deco(func): # func をラップして新しい関数を返すだけ ... |
この特性を利用すると、ロギング・認可チェック・キャッシュ など横断的な処理を コードの重複なしで 付加できます。
2. 基本的なデコレータ実装
2-1. 引数なし(シンプル)デコレータ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import functools def simple_logger(func): """呼び出しを標準出力に記録するだけの最小実装""" @functools.wraps(func) # メタ情報(__name__, __doc__ 等)の転送 def wrapper(*args, **kwargs): print(f"[LOG] {func.__name__} が呼び出されました") return func(*args, **kwargs) return wrapper @simple_logger def add(a: int, b: int) -> int: """二つの整数を足す""" return a + b print(add(3, 4)) |
ポイント
-funcを受け取り、内部でwrapperを定義して返すだけ。
-functools.wrapsの有無でデバッグ情報が大きく変わります。
2-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 |
import functools import time def timed(unit: str = "秒"): """実行時間を指定単位で出力するデコレータ""" factor = {"秒": 1, "ミリ秒": 1_000}.get(unit, 1) def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = (time.perf_counter() - start) * factor print(f"[TIME] {func.__name__}: {elapsed:.2f} {unit}") return result return wrapper return decorator @timed(unit="ミリ秒") def heavy_task(n: int): sum(i*i for i in range(n)) heavy_task(10_000) |
ポイント
- 外側の関数が設定情報(unit)を受け取り、内部で実際のラッパーwrapperを生成する「二段構造」です。
2-3. functools.wraps の正しい使い方
|
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_without_wraps(func): def wrapper(*a, **k): return func(*a, **k) return wrapper @deco_without_wraps def foo(): """元の docstring""" pass print(foo.__name__) # → 'wrapper'(情報が失われている) # 正しい例 def deco_with_wraps(func): @functools.wraps(func) def wrapper(*a, **k): return func(*a, **k) return wrapper @deco_with_wraps def bar(): """元の docstring""" pass print(bar.__name__) # → 'bar' |
wrapsを付けるだけで、関数名・docstring・シグネチャ情報がそのまま保持されます。デバッグや自動ドキュメント生成時のトラブルを防げます。
3. 標準ライブラリに用意された代表的デコレータ
| デコレータ | 主な用途 | サンプルコード |
|---|---|---|
@property |
メソッドを属性風に呼び出す(ゲッタ/セッタ) |
表示
|
@functools.lru_cache |
引数と結果のペアを自動キャッシュし、同一呼び出しを高速化 |
表示
|
@dataclass |
クラス定義から __init__, __repr__, __eq__ 等を自動生成 |
表示
|
まとめ
- いずれも「宣言だけで」大きなボイラープレートを削減します。
-propertyは API の安定化、lru_cacheは計算コストの削減、dataclassはデータ構造の可読性向上に寄与します。
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 23 24 25 26 |
import logging, functools logging.basicConfig( level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s' ) def log_calls(logger_name: str = "app"): """関数の入出力を INFO レベルで記録""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): logger = logging.getLogger(logger_name) logger.info(f"START {func.__name__} args={args} kwargs={kwargs}") result = func(*args, **kwargs) logger.info(f"END {func.__name__} → {result!r}") return result return wrapper return decorator @log_calls() def multiply(a: int, b: int) -> int: return a * b multiply(5, 7) |
ポイント
- デコレータ化するだけで、プロジェクト全体のログ出力フォーマットが統一できます。
4-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 |
import time, functools def measure_time(unit: str = "ms"): """実行時間を `unit`("s", "ms", "µs")で表示""" factors = {"s": 1, "ms": 1_000, "µs": 1_000_000} factor = factors.get(unit, 1) def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = (time.perf_counter() - start) * factor print(f"[TIME] {func.__name__}: {elapsed:.2f} {unit}") return result return wrapper return decorator @measure_time("µs") def compute(): sum(i*i for i in range(100_000)) compute() |
ポイント
-factorを変えるだけで秒・ミリ秒・マイクロ秒と柔軟に切り替え可能です。
4-3. 権限チェック(認可)デコレータ
|
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 PermissionError(RuntimeError): pass def require_role(role: str): """呼び出し側が `role` を保持しているか検証""" def decorator(func): @functools.wraps(func) def wrapper(user: dict, *args, **kwargs): if role not in user.get("roles", []): raise PermissionError(f"{user['name']} は {role} 権限がありません") return func(user, *args, **kwargs) return wrapper return decorator @require_role("admin") def delete_user(requester: dict, target_id: int): print(f"User {target_id} を削除しました(実行者: {requester['name']})") admin = {"name": "Bob", "roles": ["admin", "editor"]} guest = {"name": "Eve", "roles": ["viewer"]} delete_user(admin, 42) # 正常に実行 # delete_user(guest, 42) # PermissionError が送出 |
ポイント
- 認可ロジックを関数本体から切り離すことで、テスト容易性とコードの一貫性が向上します。
4-4. 手軽に作れるメモ化(自前キャッシュ)
|
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 |
import functools def simple_memoize(): """引数タプルをキーにした辞書ベースキャッシュ""" cache: dict[tuple, object] = {} def decorator(func): @functools.wraps(func) def wrapper(*args): if args in cache: print("キャッシュヒット") return cache[args] result = func(*args) cache[args] = result print("キャッシュ保存") return result return wrapper return decorator @simple_memoize() def slow_square(n: int) -> int: import time; time.sleep(1) # 疑似的に遅延 return n * n print(slow_square(5)) # 初回は遅い(保存) print(slow_square(5)) # 2 回目は即返却(ヒット) |
ポイント
-functools.lru_cacheが不要なシンプルケースで、キーのカスタマイズや永続化を自前実装したいときに便利です。
5. 高度利用・ベストプラクティス
5-1. 複数デコレータの適用順序
|
1 2 3 4 5 |
@log_calls() @measure_time("ms") def demo(x: int) -> int: return x * 2 |
- 評価順序は「下から上」=「最も内側(
measure_time)が先にラップし、外側(log_calls)がその結果をさらにラップ」 - 順序が変わると 計測対象範囲やログ出力タイミング が変化するため、意図した順番になるようコメントで明示すると良いでしょう。
5-2. デバッグ・テスト時の注意点
| シチュエーション | 注意点 |
|---|---|
pytest の @patch |
ラップされた関数は実際にインポートされているモジュールをパッチ対象にする必要がある(例: 'myproject.module.func')。 |
| スタックトレースの見やすさ | functools.wraps が付いていれば func.__name__ が保持され、エラー箇所が即座に特定できる。 |
| 型チェッカーとの併用 | 正しいシグネチャを保つ型注釈があると、IDE の補完や static analysis が正確になる。 |
pytest での具体例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# mymodule.py from utils import external_api @log_calls() def fetch(id_: int): return external_api(id_) # test_mymodule.py from unittest.mock import patch import mymodule def test_fetch(): with patch('mymodule.external_api') as mock_api: mock_api.return_value = {"id": 1} assert mymodule.fetch(1) == {"id": 1} mock_api.assert_called_once_with(1) |
5-3. Python 3.12+ における型ヒント付きデコレータ
正しい背景
- PEP 612(2020)で導入された
ParamSpecとConcatenateが、任意のシグネチャを保持したままラップできる型安全なデコレータを書く基礎です。 - PEP 614 は Callable の引数制約緩和であり、今回の話題とは直接関係ありません(混同しないように注意)。
実装例(Python 3.12+)
|
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 |
from __future__ import annotations import functools from typing import Callable, ParamSpec, Concatenate, TypeVar P = ParamSpec('P') R = TypeVar('R') def typed_logger(logger_name: str) -> Callable[ [Callable[Concatenate[str, P], R]], Callable[Concatenate[str, P], R] ]: """ 1 番目の引数が文字列(ログメッセージ用)である関数だけを ラップする型安全デコレータ。 """ def decorator(func: Callable[Concatenate[str, P], R]) -> Callable[Concatenate[str, P], R]: @functools.wraps(func) def wrapper(first_arg: str, *args: P.args, **kwargs: P.kwargs) -> R: import logging logger = logging.getLogger(logger_name) logger.info(f"START {func.__name__} - {first_arg}") result = func(first_arg, *args, **kwargs) logger.info(f"END {func.__name__} - {first_arg}") return result return wrapper return decorator @typed_logger("myapp") def greet(name: str) -> None: """名前を受け取って挨拶するだけの関数""" print(f"Hello, {name}!") greet("Alice") |
ParamSpecが 「残りの任意引数」 を表し、Concatenate[str, P]で 先頭に文字列 を必須としたシグネチャを構築しています。- 静的解析ツールは ラップ前後で同一シグネチャ が保たれることを認識できるため、IDE の補完や型エラー検出が正確になります。
5-4. コーディング規約のまとめ
| 項目 | 推奨事項 |
|---|---|
wraps の使用 |
すべてのデコレータで @functools.wraps を必ず付与する。 |
| ドキュメント文字列 | デコレータ本体とラッパー関数にそれぞれ簡潔な docstring を残す。 |
| 型ヒント | プロジェクトが Python 3.12 以上を対象なら ParamSpec/Concatenate を活用し、型安全性を担保する。 |
| 外部リファレンス | ブランド名やサービス名は直接記載せず「参考情報」レベルで脚注にまとめる(例: [1])。 |
6. まとめ
- デコレータは 「関数を受け取り、同じインターフェースで拡張した新しい関数を返す」 高階関数。
- 基本実装は 引数なし / 引数付き の二パターンに分かれ、
functools.wrapsが不可欠。 - 標準デコレータ(
@property,@lru_cache,@dataclass)は コード量削減・可読性向上 に直結する。 - 実務で頻出するロギング、計測、認可、キャッシュはすべて デコレータ化 でき、テストやメンテナンスが楽になる。
- 複数デコレータの適用順序・
wrapsの徹底・型ヒント(PEP 612)への対応は ベストプラクティス として必ず守るべきポイントです。
これらを踏まえて、Python プロジェクトにデコレータを導入すれば 再利用性・保守性・品質の向上 が実感できるはずです。ぜひ自分のコードベースで試してみてください。
参考文献(脚注)
- Python 標準ライブラリドキュメント –
functools,dataclasses - PEP 612 – Parameter Specification Variables (
ParamSpec) - PEP 614 – Relaxing Callable Argument Restrictions (本節で混同しないよう注意)
(※上記は公式情報に基づく一般的な参考資料です。)