既存の関数の中身を変えずにログや計測、権限チェックなどを後付けしたい時、Pythonのデコレータが強力に役立ちます。
デコレータは関数やメソッドの周りに共通処理を挟む仕組みで、関心の分離と再利用性の向上を同時に実現します。
この記事では、初心者が段階的に理解できるよう、基本から応用、注意点まで丁寧に解説します。
デコレータの基本(初心者向け): 関数を変更せず機能を拡張
デコレータとは?仕組みとメリット
デコレータとは、関数(やクラス)を引数に受け取り、別の関数(やクラス)を返す関数のことです。
適用された関数は、ラッパー関数で包まれ、呼び出し前後に共通処理を挟めます。
これにより、元の関数を改変せずにログ、計測、リトライ、権限チェックなどの機能を横断的に追加できます。
重複コードの削減とテスト容易性の向上が主なメリットです。
@記法と適用タイミング
デコレータは@decorator_nameという記法で関数定義の直前に付けます。
適用は定義時(=モジュールのインポート時)に行われ、関数オブジェクトがデコレータに渡され、decorated = decorator(original)のように置き換えられます。
副作用を持つデコレータは、インポート時にも実行される点に注意します。
最小のデコレータ例(前後でログ)
最小限の前後ログを出すデコレータを示します。
まずは概念理解のためfunctools.wrapsなしで示し、その後で改善します。
# 最小のデコレータ例: 呼び出し前後にメッセージを表示
def simple_logger(func):
def wrapper(*args, **kwargs):
print(f"[simple_logger] before: {func.__name__}")
result = func(*args, **kwargs)
print(f"[simple_logger] after: {func.__name__}")
return result
return wrapper
@simple_logger
def greet(name):
print(f"Hello, {name}!")
# 実行
greet("Alice")
[simple_logger] before: greet
Hello, Alice!
[simple_logger] after: greet
関数は第1級オブジェクトとクロージャ
Pythonでは関数は第1級オブジェクトであり、変数に代入したり引数や戻り値として扱えます。
また、クロージャにより外側スコープの値をラッパー関数が捕捉し続けます。
これがデコレータの根幹を支えています。
# 関数を引数・戻り値に使いつつ、クロージャで状態を保持する例
def call_counter(func):
count = 0 # wrapper が捕捉する外側変数(自由変数)
def wrapper(*args, **kwargs):
nonlocal count # 外側の count を更新する
count += 1
print(f"[call_counter] called {count} time(s)")
return func(*args, **kwargs)
return wrapper
@call_counter
def add(a, b):
return a + b
print(add(1, 2))
print(add(10, 20))
[call_counter] called 1 time(s)
3
[call_counter] called 2 time(s)
30
ラッパー関数と戻り値の流れ
ラッパー関数はfuncの呼び出し結果を必ず返却する必要があります。
戻り値を返し忘れるとNoneが返ってしまうため、注意が必要です。
def return_flow_logger(func):
def wrapper(*args, **kwargs):
print("[return_flow_logger] before")
value = func(*args, **kwargs) # 元関数の戻り値
print("[return_flow_logger] after")
return value # 忘れずに返す
return wrapper
@return_flow_logger
def mul(a, b):
return a * b
print(mul(6, 7))
[return_flow_logger] before
[return_flow_logger] after
42
- 関連記事:Noneとは?意味と使い方をやさしく解説
複数デコレータの適用順
デコレータを積み重ねると、適用は下から上、実行は上から下になります。
def deco_a(func):
def w(*a, **k):
print("A: before")
r = func(*a, **k)
print("A: after")
return r
return w
def deco_b(func):
def w(*a, **k):
print("B: before")
r = func(*a, **k)
print("B: after")
return r
return w
@deco_a # 2番目に適用される(実行時は外側)
@deco_b # 1番目に適用される(実行時は内側)
def work():
print("working...")
work()
A: before
B: before
working...
B: after
A: after
Pythonデコレータの使い方: 基本パターン
引数と戻り値をそのまま渡す
元関数のインターフェースを変えないことが基本です。
def passthrough(func):
def wrapper(*args, **kwargs):
# そのまま渡すだけ
return func(*args, **kwargs)
return wrapper
@passthrough
def square(x):
return x * x
print(square(5))
25
可変長引数(*args, **kwargs)の扱い
デコレータでは*argsと**kwargsで引数を丸ごと受け渡すのが安全です。
def show_args(func):
def wrapper(*args, **kwargs):
print(f"args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@show_args
def demo(a, b, c=0, **extra):
return a + b + c + sum(extra.values())
print(demo(1, 2, c=3, d=4, e=5))
args=(1, 2), kwargs={'c': 3, 'd': 4, 'e': 5}
15
functools.wrapsで関数情報を保持
デコレータは元関数の__name__や__doc__を失いがちです。
functools.wrapsを使ってメタデータをコピーします。
import functools
def with_wraps(func):
@functools.wraps(func) # ここが重要
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@with_wraps
def target(x):
"""ドキュメント文字列"""
return x
print(target.__name__)
print(target.__doc__)
target
ドキュメント文字列
例外を共通処理するデコレータ
例外をログに記録し、必要なら再送出します。
握りつぶしはバグを隠すため原則避けます。
import functools
import logging
logging.basicConfig(level=logging.INFO)
def handle_exceptions(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ValueError as e:
logging.exception("ValueError occurred")
raise # 必要に応じて再送出
return wrapper
@handle_exceptions
def might_fail(x):
if x < 0:
raise ValueError("x must be non-negative")
return x
try:
print(might_fail(-1))
except ValueError:
print("caught!")
ERROR:root:ValueError occurred
Traceback (most recent call last):
...
ValueError: x must be non-negative
caught!
- 関連記事:print()デバッグは卒業!loggingの基本設定と使い方
- 関連記事:例外(Exception)入門: 実行時エラーの正しい理解
- logging — Python 用のログ記録手段 — Python 3.13.7 ドキュメント
実行時間を計測するデコレータ
time.perf_counterで高精度計測します。
import functools
import time
def timeit(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = (time.perf_counter() - start) * 1000
print(f"[timeit] {func.__name__}: {elapsed:.2f} ms")
return wrapper
@timeit
def slow_task(n):
time.sleep(n)
return "done"
print(slow_task(0.05))
[timeit] slow_task: 50.?? ms
done
- time.perf_counter() — 時刻データへのアクセスと変換 — Python 3.13.7 ドキュメント
- 関連記事:実行時間を計測する方法まとめ(timeitとperf_counter)
ログ・トレースを追加するデコレータ
引数と戻り値をトレースします。
機密情報には配慮します。
import functools
import logging
logging.basicConfig(level=logging.INFO)
def trace(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info("→ %s args=%s kwargs=%s", func.__name__, args, kwargs)
result = func(*args, **kwargs)
logging.info("← %s result=%r", func.__name__, result)
return result
return wrapper
@trace
def concat(a, b, sep="-"):
return f"{a}{sep}{b}"
print(concat("A", "B", sep=":"))
INFO:root:→ concat args=('A', 'B') kwargs={'sep': ':'}
INFO:root:← concat result='A:B'
A:B
権限チェック(認可)のデコレータ
簡易なロールチェックの例です。
実務ではフレームワークの仕組みに合わせます。
import functools
def require_role(role_name):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
# 第一引数 user の roles を想定
if role_name not in getattr(user, "roles", set()):
raise PermissionError(f"role '{role_name}' required")
return func(user, *args, **kwargs)
return wrapper
return decorator
class User:
def __init__(self, name, roles):
self.name = name
self.roles = set(roles)
@require_role("admin")
def delete_post(user, post_id):
return f"{post_id} deleted by {user.name}"
alice = User("Alice", ["admin"])
bob = User("Bob", [])
print(delete_post(alice, 123))
try:
print(delete_post(bob, 123))
except PermissionError as e:
print("denied:", e)
123 deleted by Alice
denied: role 'admin' required
- 関連記事:raiseの使い方: 基本と実例
応用デコレータ: 引数付き・メソッド・クラス
引数付きデコレータの作り方
デコレータファクトリで外側にパラメータを受け取ります。
import functools
def tag(prefix="*"):
# ここがファクトリ: 引数を受け取る
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix} begin")
r = func(*args, **kwargs)
print(f"{prefix} end")
return r
return wrapper
return tag # ← ではなく、decorator を返すのが正解
# 注意: 上の return は decorator を返す必要がある
def tag(prefix="*"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix} begin")
r = func(*args, **kwargs)
print(f"{prefix} end")
return r
return wrapper
return decorator
@tag(prefix=">>>")
def hello():
print("hello")
hello()
>>> begin
hello
>>> end
オプション付きログ(レベルやメッセージ)
ログレベルを可変にします。
import functools
import logging
logging.basicConfig(level=logging.DEBUG)
def log(level=logging.INFO, msg="call"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.log(level, "%s: %s", msg, func.__name__)
return func(*args, **kwargs)
return wrapper
return decorator
@log(level=logging.DEBUG, msg="start")
def compute(x, y):
return x + y
print(compute(3, 4))
DEBUG:root:start: compute
7
- 関連記事:loggingの基本設定と使い方
メソソッドへの適用とself/cls
インスタンスメソッドではself、クラスメソッドではclsが先頭引数になります。
デコレータ側は特別扱いは不要で、そのまま受け取ります。
import functools
def show_bound(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("first arg type:", type(args[0]).__name__) # self or cls
return func(*args, **kwargs)
return wrapper
class Calc:
@show_bound
def add(self, a, b):
return a + b
@classmethod
@show_bound
def version(cls):
return "1.0"
c = Calc()
print(c.add(2, 3))
print(Calc.version())
first arg type: Calc
5
first arg type: type
1.0
組み込みデコレータ(@staticmethod/@classmethod/@property)
組み込みデコレータの役割と、他デコレータとの順序の基本を整理します。
@staticmethod:self不要な関数をメソッドとして束縛@classmethod: 最初の引数にclsが渡る@property: ゲッターをプロパティ化
順序は意味を変えるため注意が必要です。
- @property — Python 3.13.7 ドキュメント
- @classmethod — Python 3.13.7 ドキュメント
- @staticmethod — Python 3.13.7 ドキュメント
一般に@propertyは最も内側(下)に置き、その外側に独自デコレータを重ねます。
import functools
def uppercase(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
class User:
def __init__(self, name):
self._name = name
@property # 内側: ゲッター定義
@uppercase # 外側: 返り値を加工
def name(self):
return self._name
u = User("alice")
print(u.name)
ALICE
- 関連記事: @property の使い方まとめ
クラスデコレータの基本
クラスを受け取り、修正または別のクラスを返します。
簡単な登録・属性追加の例です。
REGISTRY = {}
def register(name):
def decorator(cls):
REGISTRY[name] = cls
cls.registered_as = name # 動的に属性追加
return cls
return decorator
@register("service")
class Service:
pass
print(Service.registered_as)
print(REGISTRY["service"] is Service)
service
True
- 関連記事:はじめてのクラス: class文の書き方
ベストプラクティスと注意点
デコレータの順序と副作用
複数デコレータは下から適用、上から実行です。
初期化処理や副作用があるデコレータを外側に置くか内側に置くかで挙動が変わります。
順序はテストで明示的に検証します。
- 関連記事:テスト容易性を上げる関数の書き方とNG例
import時に実行される点に注意
@decoは定義時に評価されます。
重い初期化や外部接続をデコレータ内で行うと、インポートだけで遅くなるため避けます。
必要なら遅延初期化(初回呼び出し時に実行)にします。
例外を握りつぶさない設計
デバッグや監視のためログは取りつつ、適切に再送出します。
bare except(例: except:)は避け、想定例外だけを捕捉します。
型ヒント(Callable/TypeVar/ParamSpec)のコツ
ジェネリックに正しい型を保つにはParamSpecとTypeVarを使います。
これにより、デコレータが元関数の引数型と戻り値型をそのまま表現できます。
from typing import Callable, TypeVar, ParamSpec
import functools
P = ParamSpec("P")
R = TypeVar("R")
def typed_decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper
@typed_decorator
def f(x: int, y: str) -> float:
return float(x + len(y))
print(f(3, "ab"))
5.0
- typing.ParamSpec — Support for type hints — Python 3.13.7 ドキュメント
- typing.TypeVar — Support for type hints — Python 3.13.7 ドキュメント
- 関連記事:型ヒントの書き方と使い方: typingとmypyまで
デコレータを一時無効化する手法
開発やデバッグで一時的に無効化したい場合があります。
import functools
import os
ENABLE_TRACE = os.getenv("ENABLE_TRACE", "0") == "1"
def optional_trace(func):
if not ENABLE_TRACE:
return func # 何もしない(パススルー)
@functools.wraps(func)
def wrapper(*a, **k):
print("[trace]", func.__name__, a, k)
return func(*a, **k)
return wrapper
@optional_trace
def calc(a, b):
return a + b
print(calc(1, 2))
3
環境変数ENABLE_TRACE=1で有効化できます。
また、if DEBUG: decorator = identityのように差し替える方法も有効です。
- 関連記事:if文の基本と書き方
パフォーマンスと過度な抽象化を避ける
デコレータは呼び出しコストをわずかに増やします。
ホットパスに多段適用するとオーバーヘッドが顕著になる場合があります。
計測(timeit)して、必要ならデコレータの層を減らす、ロジックをまとめる、または@functools.lru_cacheなど計算量削減の施策を検討します。
以下は重ねがけのオーバーヘッドを観察する簡単な比較例です。
import functools
import time
def noop(func):
@functools.wraps(func)
def w(*a, **k):
return func(*a, **k)
return w
@noop
@noop
@noop
def fast(x):
return x + 1
start = time.perf_counter()
for _ in range(1_000_0):
fast(1)
print("elapsed:", (time.perf_counter() - start) * 1000, "ms")
elapsed: X.XX ms
実運用では、可読性と保守性を損なうほどの抽象化は避け、役割の明確なデコレータに留めます。
まとめ
デコレータは、元の関数を変更せずに機能を拡張できるPythonの強力な機能です。
仕組みの要は、関数が第1級オブジェクトであることと、クロージャで外側の状態を保持できることにあります。
基本では*argsと**kwargsの透過、functools.wrapsによるメタデータ保持、例外の適切な再送出、計測やログの追加を押さえます。
応用では、引数付きデコレータ、メソッドやproperty、クラスデコレータまで広げられます。
最後に、インポート時に評価される点や順序・副作用、パフォーマンスに注意し、必要に応じて無効化や型ヒントで安全性と開発体験を高めていきましょう。
