2026年9月13日
2026年9月13日
Pythonのデコレーターの使い方と実装方法|仕組み・functools.wraps・引数付き・リトライの実例
Python のデコレーターは、関数を受け取って「機能を足した別の関数」を返す仕組みで、@名前 を関数定義の上に書くだけで、処理時間の計測やリトライ、ログ出力といった共通の処理を元の関数を書き換えずに付け足せます。この記事では、デコレーターが内部で何をしているのかを確認したうえで、functools.wraps を使った正しい書き方、引数を受け取るデコレーター、複数を重ねたときの順番、async 関数への対応、型ヒントの付け方までを、運用スクリプトで使える実例と一緒に説明します。
デコレーターの正体
Python では関数もオブジェクトなので、変数に入れたり、引数として渡したり、戻り値として返したりできます。デコレーターはこの性質を使った書き方です。
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
@shout
def greet():
return "hello"
print(greet()) # HELLO
@shout は、次の1行とまったく同じ意味です。
greet = shout(greet)
つまり greet という名前は、元の関数ではなく shout が返した wrapper を指すようになります。呼び出すと wrapper が動き、その中で元の関数を呼んで結果を加工しています。この「名前が別の関数に差し替わる」点を理解しておくと、この後に出てくる注意点の理由が分かりやすくなります。
実用の基本形:*args・**kwargs と functools.wraps
上の例は引数のない関数にしか使えません。どんな関数にも付けられるようにするには、引数を *args, **kwargs でそのまま受け渡します。処理時間を計測してログに出すデコレーターを例にします。
import functools
import logging
import time
log = logging.getLogger(__name__)
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
log.info("%s: %.3f 秒", func.__qualname__, elapsed)
return wrapper
@timed
def export_db(path: str) -> None:
"""データベースをエクスポートする。"""
time.sleep(0.5) # 実際の処理の代わり
logging.basicConfig(level=logging.INFO)
export_db("/backup/db.sql")
print(export_db.__name__, export_db.__doc__)
# export_db データベースをエクスポートする。
ここには3つのポイントがあります。
**return func(*args, **kwargs) を忘れない。** wrapper が元の関数の戻り値を返さないと、デコレーターを付けただけで戻り値が None になります。初心者が最もつまずきやすい点です。
**try / finally で計測する。** 元の関数が例外を出しても、かかった時間はログに残ります。例外は握りつぶさずにそのまま呼び出し元へ伝わります。
**@functools.wraps(func) を付ける。** これがないと、export_db.__name__ が wrapper になり、docstring も失われます。ログやエラーのトレースバック、help() の表示が分かりにくくなるうえ、関数名を使うテストツールやフレームワークで不具合の原因になることもあります。wraps は名前や docstring をコピーし、元の関数を __wrapped__ 属性に残してくれます。
引数を受け取るデコレーター:リトライの例
@retry(times=3) のように引数を渡したい場合は、関数をもう1段重ねます。「引数を受け取って、デコレーターを返す関数」を作るイメージです。
import functools
import logging
import time
log = logging.getLogger(__name__)
def retry(times: int = 3, delay: float = 1.0, exceptions: tuple = (Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times:
raise
wait = delay * 2 ** (attempt - 1)
log.warning("%s 失敗(%d/%d): %s。%.1f 秒後に再試行",
func.__name__, attempt, times, e, wait)
time.sleep(wait)
return wrapper
return decorator
@retry(times=4, delay=2.0, exceptions=(ConnectionError, TimeoutError))
def fetch_status(url: str) -> str:
...
@retry(times=4, ...) は、まず retry(times=4, ...) が呼ばれて decorator が返り、それが fetch_status に適用される、という2段階で動きます。待ち時間は 2、4、8 秒と倍に延ばし、最後の試行でも失敗したら raise で元の例外をそのまま投げ直しています。
リトライ対象の例外は、通信エラーのように「もう一度やれば成功しうるもの」に絞るのが大切です。Exception 全体を対象にすると、引数の誤りのような何度やっても失敗するエラーまで繰り返して、原因の発見が遅れます。また、決済やメール送信など、途中まで成功していると二重に実行されて困る処理には、安易にリトライを付けないようにします。
なお、@retry とかっこなしで書くと、関数そのものが times に渡されてしまい、意図どおりに動きません。引数付きのデコレーターは @retry() のようにかっこを付けて使います。
複数のデコレーターを重ねたときの順番
デコレーターは重ねて付けられます。適用されるのは下(関数に近い側)からです。
@timed
@retry(times=3)
def sync_files():
...
# 次と同じ意味
# sync_files = timed(retry(times=3)(sync_files))
この場合、外側の timed は「リトライを含めた全体の時間」を計測します。上下を入れ替えると、retry が timed 付きの関数を繰り返し呼ぶことになり、1回ごとの時間がログに出ます。どちらが正しいということはなく、何を測りたいかで順番を決めます。
async 関数に付けるデコレーター
async def で定義した関数に、上の timed をそのまま付けると、wrapper は「コルーチンを作って返す」だけで終わるため、実際の処理時間を測れません。async 関数用には wrapper も async にします。
import functools
import inspect
import time
def timed_any(func):
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return await func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.3f} 秒")
return async_wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.3f} 秒")
return wrapper
非同期処理の中で待つときは time.sleep() ではなく await asyncio.sleep() を使う点も、リトライ処理を async 版にするときの注意点です。
型ヒントを付ける
型チェッカーを使っている場合、デコレーターを付けると元の関数の引数情報が失われたように見えることがあります。Python 3.10 以降では ParamSpec を使うと、引数と戻り値の型を保ったまま書けます。サポートが続いているバージョンであれば使えます(2026年10月現在)。
import functools
import time
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def timed(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.3f} 秒")
return wrapper
標準ライブラリにあるデコレーター
自分で書く前に、標準で用意されているものを知っておくと便利です。
@property、@staticmethod、@classmethod:クラスのメソッドの性質を変える@functools.cache、@functools.lru_cache(maxsize=...):同じ引数の結果を記憶して再計算を省く。引数はハッシュ化できる値である必要があり、cacheは上限なく記憶するため、引数の種類が多い関数ではメモリに注意する@functools.cached_property:最初に読んだときだけ計算するプロパティ@contextlib.contextmanager:ジェネレーター関数からwith文で使えるオブジェクトを作る@dataclasses.dataclass:クラスに付けて__init__などを自動生成する(クラスにも付けられるデコレーターの例)
まとめ
@decoはfunc = deco(func)と同じ。名前が wrapper に差し替わる- wrapper は
*args, **kwargsを受け渡し、元の関数の戻り値を返す @functools.wraps(func)で関数名・docstring を引き継ぐ- 引数付きデコレーターは1段増やして書き、リトライは対象の例外を絞る
- 重ねたときは下から適用される。async 関数には async の wrapper を用意する