Contents
1️⃣ デコレータとは? @ 記号の役割
要点
デコレータは「関数(またはクラス)を受け取り、別のオブジェクト(ラッパー)を返す」高階関数です。
@decorator と書くと内部的には
|
1 2 |
func = decorator(func) |
が実行され、シンタックスシュガーとしてコードを簡潔に保ちます。
参考
- Stack Overflow: What is a Python decorator? (2023‑04‑12) [link]
1.1 基本例(@wraps を忘れない)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from functools import wraps def simple_decorator(func): @wraps(func) # ← メタデータを保持 def wrapper(*args, **kwargs): print("Before call") result = func(*args, **kwargs) print("After call") return result return wrapper @simple_decorator def greet(name: str) -> None: """名前を出力するだけの関数""" print(f"Hello, {name}") greet("Alice") |
実行結果
|
1 2 3 4 |
Before call Hello, Alice After call |
@wrapsが無いと、wrapper.__name__や docstring が失われ、デバッグや自動ドキュメント生成が困難になります。
2️⃣ 最小限の関数デコレータ実装例
2.1 汎用ロギングデコレータ(@wraps 必須)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from functools import wraps from typing import Callable, TypeVar, Any, cast F = TypeVar("F", bound=Callable[..., Any]) def log_calls(func: F) -> F: """呼び出し情報を標準出力に記録するデコレータ""" @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: arg_str = ", ".join(repr(a) for a in args) kw_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) print(f"Calling {func.__name__}({arg_str}{', ' if kw_str else ''}{kw_str})") result = func(*args, **kwargs) print(f"{func.__name__} returned {result!r}") return result return cast(F, wrapper) # 型安全のためにキャスト |
|
1 2 3 4 5 6 7 |
@log_calls def add(a: int, b: int) -> int: """2 つの整数を足すだけ""" return a + b add(3, 5) |
実行結果
|
1 2 3 |
Calling add(3, 5) add returned 8 |
Qiita の解説でも同様の実装が推奨されています(2022‑11‑05)[link]。
2.2 *args, **kwargs と型ヒントで汎用化
- 任意のシグネチャに対応できるので、コードレビューが楽になる
- IDE 補完や
mypyのチェックも通ります
3️⃣ 複数デコレータの適用順序と可視化
3.1 順序の原則
| ステップ | 実際に行われること |
|---|---|
| 評価 | func = outer(inner(func)) (下から上) |
| 呼び出し | outer.wrapper → inner.wrapper → func(上から下) |
3.2 デモコード(@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 |
from functools import wraps def deco_outer(func): @wraps(func) def wrapper(*args, **kwargs): print("outer start") result = func(*args, **kwargs) print("outer end") return result return wrapper def deco_inner(func): @wraps(func) def wrapper(*args, **kwargs): print("inner start") result = func(*args, **kwargs) print("inner end") return result return wrapper @deco_outer @deco_inner def hello(): print("Hello world") hello() |
実行結果
|
1 2 3 4 5 6 |
outer start inner start Hello world inner end outer end |
Zenn の記事で図解が分かりやすく紹介されています(2023‑02‑14)[link]。
3.3 デバッグヒント
printやロギングで ラッパーの開始・終了 を出力すると、意図しない順序をすぐに発見できます。
4️⃣ 標準ライブラリデコレータとクラス/メソッドへの適用
| デコレータ | 主な用途 | 注意点 |
|---|---|---|
@property |
属性アクセスのゲッタ/セッタ統合 | 書き換え可能にしたい場合は setter を実装 |
@functools.lru_cache |
計算結果キャッシュ | 引数がハッシュ可能であること |
@dataclasses.dataclass |
データクラス生成 | フィールドのデフォルト設定に注意 |
4.1 @property の実装例(@wraps は不要)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Circle: def __init__(self, radius: float) -> None: self._radius = radius @property def radius(self) -> float: """半径を取得するだけの読み取り専用属性""" return self._radius @radius.setter def radius(self, value: float) -> None: if value <= 0: raise ValueError("Radius must be positive") self._radius = value @property def area(self) -> float: """半径から計算した面積(読み取り専用)""" import math return math.pi * self._radius ** 2 |
4.2 @functools.lru_cache の活用例
|
1 2 3 4 5 6 7 8 9 10 11 |
import functools @functools.lru_cache(maxsize=64) def fib(n: int) -> int: """フィボナッチ数列(再帰)をキャッシュ付きで計算""" if n < 2: return n return fib(n - 1) + fib(n - 2) print(fib(35)) # 初回は遅いが、二度目以降は即座に取得できる |
4.3 カスタムクラスデコレータ(@wraps は不要)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from datetime import datetime def add_timestamp(cls): """クラス定義時に `created_at` 属性を付与""" cls.created_at = datetime.now() return cls @add_timestamp class Task: def __init__(self, name: str) -> None: self.name = name t = Task("Demo") print(t.created_at) |
クラスデコレータは インスタンス生成時に呼ばれない こと、継承先でも属性が共有される可能性がある点を留意してください。
5️⃣ 実務で役立つ応用例 ― ログ・計測・認証チェック
5.1 共通基盤としてのロギングデコレータ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import logging from functools import wraps from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") def log_execution(func: F) -> F: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: logging.info("Start %s args=%s kwargs=%s", func.__name__, args, kwargs) result = func(*args, **kwargs) logging.info("End %s return=%r", func.__name__, result) return result return wrapper # type: ignore |
|
1 2 3 4 5 |
@log_execution def fetch_user(user_id: int) -> dict: """疑似的にユーザ情報を取得する関数""" return {"id": user_id, "name": f"User{user_id}"} |
5.2 処理時間計測デコレータ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import time from functools import wraps from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) def timing(func: F) -> F: @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"{func.__name__} executed in {elapsed_ms:.2f} ms") return result return wrapper # type: ignore |
|
1 2 3 4 5 6 7 |
@timing def heavy_computation(n: int) -> int: total = 0 for i in range(n): total += i * i return total |
5.3 権限チェックデコレータ(require_role)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from functools import wraps from typing import Callable, TypeVar, Any F = TypeVar("F", bound=Callable[..., Any]) class PermissionError(Exception): """権限が不足しているときに送出する例外""" def require_role(role: str): def decorator(func: F) -> F: @wraps(func) def wrapper(user: dict, *args: Any, **kwargs: Any) -> Any: if user.get("role") != role: raise PermissionError(f"User lacks required role: {role}") return func(user, *args, **kwargs) return wrapper # type: ignore return decorator |
|
1 2 3 4 |
@require_role("admin") def delete_record(user: dict, record_id: int) -> None: print(f"Record {record_id} deleted by {user['name']}") |
5.4 テスト例(unittest と pytest)
unittest
|
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 unittest from unittest.mock import patch class TestDecorators(unittest.TestCase): def test_log_execution(self): with self.assertLogs(level="INFO") as cm: fetch_user(1) self.assertIn("Start fetch_user", cm.output[0]) self.assertIn("End fetch_user", cm.output[-1]) def test_timing(self): with patch("builtins.print") as mock_print: heavy_computation(1000) mock_print.assert_called_once() self.assertRegex(mock_print.call_args[0][0], r"heavy_computation executed in \d+\.\d+ ms") def test_require_role_success(self): delete_record({"name": "Alice", "role": "admin"}, 7) # 例外が出なければ成功 def test_require_role_failure(self): with self.assertRaises(PermissionError): delete_record({"name": "Bob", "role": "guest"}, 7) if __name__ == "__main__": unittest.main() |
pytest
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import pytest from unittest.mock import MagicMock def test_log_execution_pytest(caplog): fetch_user(2) assert "Start fetch_user" in caplog.text assert "End fetch_user" in caplog.text def test_timing_calls(monkeypatch): mock_print = MagicMock() monkeypatch.setattr("builtins.print", mock_print) heavy_computation(500) mock_print.assert_called_once() |
5️⃣ パフォーマンス考慮
| 項目 | 推奨策 |
|---|---|
| オーバーヘッド | デコレータは必ず 1 回余分な関数呼び出しを行う。極端に頻繁なループではプロファイリングが必須 |
| 軽量化 | ラッパー内部には最小限のロジックだけ入れ、重い処理は元関数へ委譲 |
| キャッシュ活用 | functools.lru_cache などでラッパー自体のコストを相殺 |
| プロファイリング | cProfile, pyinstrument 等で全体実行時間を測定し、ボトルネックかどうか判断 |
6️⃣ まとめ(簡潔版)
- デコレータは
@によるシンタックスシュガー。内部では関数呼び出しに置き換わります。 - 汎用ラッパーは
*args, **kwargsと型ヒントで実装し、必ずfunctools.wrapsを付与してメタデータを保護します。 - 複数デコレータの評価順序は内側→外側、呼び出しは逆順。
printやロギングで可視化すると安全です。 - 標準デコレータ (
@property,@lru_cache) は実務で即戦力。カスタムクラスデコレータは継承や属性共有に注意が必要です。 - ログ・計測・認証チェックは共通基盤としてデコレータ化し、テストとプロファイリングで品質を担保します。
参考文献
| # | タイトル | 出典 | アクセス日 |
|---|---|---|---|
| 1 | What is a Python decorator? | Stack Overflow (2023) | 2026‑04‑10 |
| 2 | デコレータの基本実装例 | Qiita, dakikd (2022‑11‑05) | 2026‑04‑10 |
| 3 | Python デコレータ入門 | Zenn, ap_com (2023‑02‑14) | 2026‑04‑10 |
| 4 | Python の property 解説 | frkz.jp (2021‑08‑20) | 2026‑04‑11 |
| 5 | functools.lru_cache の使いどころ | Python Docs (最新版) | 2026‑04‑12 |
※外部リンクは執筆時点で有効な URL を確認済みです。
次のステップ
1. 本記事のサンプルを自分のプロジェクトにコピーし、@wraps が正しく入っているかコンパイルエラー無しで動作することを確認。
2. 既存コードベースで「共通的な前処理(ログ・計測・権限)」がない箇所を洗い出し、上記デコレータのどれかまたは組み合わせで置き換える。
3. cProfile や pytest --cov でパフォーマンスとテストカバレッジを測定し、必要に応じて lru_cache 等で最適化する。
この記事は 2026‑04‑17 に更新されました。