関数やクラスのソースを直接変更せずに、ログ出力や実行時間計測、入力チェックなどの機能を後付けできるのがデコレータです。
本記事では、初心者の方にもわかるように、@記法の基本から実践的なユースケース、メソッドやクラスへの適用まで段階的に解説します。
PEP8に沿った書き方や型ヒントも取り入れます。
初心者向け: Pythonデコレータの基本
デコレータとは 関数を変更せず機能を拡張
デコレータは、関数(やメソッド、クラス)を引数に受け取り、拡張した新しい関数(やクラス)を返す高階関数です。
元の関数の振る舞いをラップ(wrapper)して、前後に処理を挿入したり、例外処理やログを追加したりできます。
これにより、関心の分離を保ったまま再利用可能な横断的関心事(ロギングや認可など)を実現できます。
@記法と高階関数の仕組み
Pythonではデコレータを@decorator_name
というシンタックスシュガーで適用します。
実体は「関数を関数に渡して戻り値で置き換える」操作です。
# デコレータの最小例
from typing import Callable
def hello_decorator(func: Callable) -> Callable:
def wrapper():
print("前処理: Hello")
result = func()
print("後処理: Bye")
return result
return wrapper
# @記法の利用
@hello_decorator
def greet():
print("中身: greetが呼ばれました")
# 上の@は次と等価です:
# greet = hello_decorator(greet)
greet()
前処理: Hello
中身: greetが呼ばれました
後処理: Bye
はじめてのデコレータの使い方
最初は引数も戻り値もない関数で感覚を掴むのが安全です。
PEP8に従って、関数名はsnake_case
で書き、内側の関数名はwrapper
など意味が伝わる名前にします。
def notify(func):
"""呼び出しの前後でメッセージを表示する簡単なデコレータ"""
def wrapper():
print("[notify] start")
value = func()
print("[notify] end")
return value
return wrapper
@notify
def say():
print("Hi!")
say()
[notify] start
Hi!
[notify] end
デコレータの実装とベストプラクティス
wrapperと*args **kwargsの受け渡し
実務では引数や戻り値がある関数をラップする必要があります。
そのため、*args
と**kwargs
であらゆる引数を受け取り、元の関数にそのまま渡します。
from typing import Any, Callable
def debug_args(func: Callable[..., Any]) -> Callable[..., Any]:
"""引数と戻り値を表示するデコレータ"""
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"[debug_args] args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[debug_args] result={result!r}")
return result
return wrapper
@debug_args
def add(a: int, b: int, scale: float = 1.0) -> float:
return (a + b) * scale
add(2, 3, scale=0.5)
[debug_args] args=(2, 3), kwargs={'scale': 0.5}
[debug_args] result=2.5
functools.wrapsでメタデータを保持
デコレータを適用すると元の関数名や__doc__
が失われがちです。
functools.wraps
でメタデータをコピーしましょう。
ドキュメント生成やデバッグ、型チェッカーとの相性が良くなります。
from functools import wraps
from typing import TypeVar, Callable, Any
F = TypeVar("F", bound=Callable[..., Any])
def preserve_meta(func: F) -> F:
"""wrapsを使ってメタデータを保持する例"""
@wraps(func) # これが重要です
def wrapper(*args: Any, **kwargs: Any):
"""内側のdocstringは外に見えなくなる(元のdocstringが保持される)"""
return func(*args, **kwargs)
return wrapper # type: ignore[return-value]
@preserve_meta
def area(radius: float) -> float:
"""半径から円の面積を計算します"""
from math import pi
return pi * radius * radius
print(area.__name__) # 元の関数名が保たれる
print(area.__doc__) # 元のdocstringが保たれる
area
半径から円の面積を計算します
引数ありデコレータの作り方
デコレータ自身に設定値を渡したいことがあります。
その場合は「関数を返す関数を返す」三層構造になります。
from functools import wraps
from typing import Callable, Any
def repeat(times: int = 2) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""指定回数だけ関数を繰り返し実行するデコレータファクトリ"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
result = None
for i in range(times):
print(f"[repeat] {i+1}/{times}")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def ping() -> str:
print("pong")
return "ok"
ping()
[repeat] 1/3
pong
[repeat] 2/3
pong
[repeat] 3/3
pong
複数デコレータの適用順と注意点
デコレータは「下から先に適用」されます。
ソース上でdef
に近いものが最初に関数へ作用し、その戻り値に対して次のデコレータが適用されます。
順序が挙動に影響するため、ログとキャッシュなどを組み合わせる場合は意図を明確にしましょう。
def A(func):
def wrapper(*a, **k):
print("A: before")
r = func(*a, **k)
print("A: after")
return r
return wrapper
def B(func):
def wrapper(*a, **k):
print("B: before")
r = func(*a, **k)
print("B: after")
return r
return wrapper
@A
@B
def work():
print("do work")
work()
A: before
B: before
do work
B: after
A: after
上の例では@B
が先にwork
へ適用され、次に@A
が適用されています。
例外処理と戻り値の扱い
デコレータ側で例外を握りつぶすと、呼び出し元でバグを検知できなくなります。
原則は「ログして再送出(raise)」か「仕様として明確にデフォルトを返す」です。
from functools import wraps
from typing import Any, Callable, Optional, Type, Tuple
def safe(default: Optional[Any] = None,
suppress: Tuple[Type[BaseException], ...] = (Exception,)) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
例外をログし、指定の例外のみ抑制してdefaultを返す。
それ以外は再送出します。
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return func(*args, **kwargs)
except suppress as e:
print(f"[safe] suppressed: {e!r}")
return default
return wrapper
return decorator
@safe(default=-1, suppress=(ZeroDivisionError,))
def div(a: int, b: int) -> float:
return a / b
print(div(4, 2)) # 正常
print(div(1, 0)) # 例外を抑制しdefaultを返す
2.0
[safe] suppressed: ZeroDivisionError('division by zero')
-1
デコレータのユースケース
ログ出力と実行時間の計測
関数の開始と終了、実行時間を測るのは定番です。
time.perf_counter()
で高精度に計測します。
import time
from functools import wraps
from typing import Any, Callable
def timeit(label: str | None = None) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""実行時間を計測して表示するデコレータ"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
name = label or func.__name__
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
dur = (time.perf_counter() - start) * 1000
print(f"[timeit] {name}: {dur:.2f} ms")
return wrapper
return decorator
@timeit()
def slow(n: int) -> int:
time.sleep(0.05)
return n * 2
print(slow(5))
[timeit] slow: 50.?? ms
10
(実行時間は環境により変動します)
権限チェックと入力バリデーション
認可と入力チェックは関数の外側に置くと見通しが良くなります。
from dataclasses import dataclass
from functools import wraps
from typing import Callable, Any
@dataclass
class User:
name: str
is_admin: bool = False
def requires_admin(func: Callable[..., Any]) -> Callable[..., Any]:
"""第1引数userの権限をチェックするデコレータ"""
@wraps(func)
def wrapper(user: User, *args: Any, **kwargs: Any) -> Any:
if not user.is_admin:
raise PermissionError("admin権限が必要です")
return func(user, *args, **kwargs)
return wrapper
def validate_positive(func: Callable[..., Any]) -> Callable[..., Any]:
"""数値引数が正であることをバリデートする例"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
for x in args:
if isinstance(x, (int, float)) and x <= 0:
raise ValueError("引数は正の数である必要があります")
return func(*args, **kwargs)
return wrapper
@requires_admin
@validate_positive
def create_quota(user: User, size_gb: int) -> str:
return f"{user.name} に {size_gb}GB のクォータを作成しました"
admin = User("Alice", is_admin=True)
guest = User("Bob", is_admin=False)
print(create_quota(admin, 10))
# print(create_quota(guest, 10)) # PermissionError
Alice に 10GB のクォータを作成しました
キャッシュやリトライの導入方針
計算コストが高い処理は結果をキャッシュし、外部I/Oは一時的失敗をリトライすると安定します。
標準のfunctools.lru_cache
で簡単にメモ化できます。
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(30)) # 高速に計算される(以降はキャッシュ)
832040
リトライはパラメータ化されたデコレータで柔軟に実装できます。
import time
from functools import wraps
from typing import Callable, Any, Tuple, Type
def retry(retries: int = 3, delay: float = 0.1,
exceptions: Tuple[Type[BaseException], ...] = (Exception,)) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""一時的な失敗をリトライするデコレータ"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
last = None
for attempt in range(1, retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last = e
print(f"[retry] attempt {attempt}/{retries} failed: {e!r}")
if attempt < retries:
time.sleep(delay)
assert last is not None
raise last
return wrapper
return decorator
count = {"n": 0}
@retry(retries=3, delay=0.05, exceptions=(RuntimeError,))
def flaky() -> str:
count["n"] += 1
if count["n"] < 3:
raise RuntimeError("temporary failure")
return "success"
print(flaky())
[retry] attempt 1/3 failed: RuntimeError('temporary failure')
[retry] attempt 2/3 failed: RuntimeError('temporary failure')
success
メソッドとクラスのデコレータ
メソッドにデコレータを適用するポイント
インスタンスメソッドでもself
は通常の第1引数として渡されます。
*args
と**kwargs
で受けてそのまま渡せば問題ありません。
from functools import wraps
from typing import Any, Callable
def trace(func: Callable[..., Any]) -> Callable[..., Any]:
"""呼び出しをトレースするデコレータ"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"[trace] {func.__qualname__} called")
return func(*args, **kwargs)
return wrapper
class Calculator:
@trace
def mul(self, a: int, b: int) -> int:
return a * b
c = Calculator()
print(c.mul(3, 4))
[trace] Calculator.mul called
12
@staticmethodと@classmethodの使い分け
@staticmethod
はself
やcls
を受け取らない関数をクラスの名前空間にまとめたいときに使います。@classmethod
はクラス自身を第1引数cls
で受け取り、オルタナティブコンストラクタやクラス全体に関する処理に使います。
これらに独自デコレータを併用する場合、基本的には「独自デコレータをdef
に近く、その上に@staticmethod
や@classmethod
」の順が安全です。
こうすると独自デコレータが素の関数をラップし、最後にメソッド化されます。
from functools import wraps
from typing import Any, Callable
def tag(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def deco(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*a: Any, **k: Any) -> Any:
print(f"[{name}] {func.__name__}")
return func(*a, **k)
return wrapper
return deco
class Util:
@staticmethod # 外側: メソッド化
@tag("static") # 内側: 素の関数を装飾
def identity(x: int) -> int:
return x
@classmethod # 外側: メソッド化
@tag("class") # 内側: 素の関数を装飾
def from_str(cls, s: str) -> tuple[type, str]:
return (cls, s)
print(Util.identity(7))
print(Util.from_str("ok"))
[static] identity
7
[class] from_str
(<class '__main__.Util'>, 'ok')
クラスデコレータでクラスに機能追加
クラス全体を受け取り、属性を追加したり登録処理を行うのがクラスデコレータです。
軽量なメタクラスの代替として有用です。
from functools import wraps
from typing import Type, Any, Dict
def auto_repr(cls: Type[Any]) -> Type[Any]:
"""
__repr__が未定義なら、自動でフィールドから生成する。
PEP8に沿い、クラスはCapWords、関数はsnake_caseで命名します。
"""
if "__repr__" in cls.__dict__:
return cls
def __repr__(self) -> str:
fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
return f"{cls.__name__}({fields})"
setattr(cls, "__repr__", __repr__)
return cls
REGISTRY: Dict[str, Type[Any]] = {}
def register(name: str):
"""クラスをレジストリに登録するクラスデコレータ"""
def decorator(cls: Type[Any]) -> Type[Any]:
REGISTRY[name] = cls
return cls
return decorator
@register("point")
@auto_repr
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
p = Point(2, 3)
print(p) # 自動生成されたrepr
print(REGISTRY["point"]) # レジストリに登録済み
Point(x=2, y=3)
<class '__main__.Point'>
まとめ
デコレータは「関数やクラスを引数に受けて拡張したものを返す」高階関数であり、既存コードを変更せずに機能を追加できる強力な仕組みです。
基本は*args
と**kwargs
で汎用ラッパーを作り、functools.wraps
でメタデータを保護することです。
複数デコレータの適用順や、例外の扱い(再送出かデフォルト返却か)を明確にし、ログ出力、実行時間計測、認可、バリデーション、キャッシュ、リトライといった横断的関心事を整理して適用します。
メソッドでは@staticmethod
や@classmethod
との組み合わせ順に注意し、クラスデコレータで振る舞いを横断的に付与する設計も有効です。
PEP8に沿った命名と型ヒントを添えることで、可読性と保守性がさらに高まります。
まずは小さな関数から装飾を試し、ユースケースに合わせて少しずつ抽象度を上げていくと着実に使いこなせます。