Pythonのデコレータ入門!@1行で処理時間の計測・リトライ・キャッシュを付け足す方法
「@って、関数の上に付いてるあれ、なに?」「Flaskやpytestのサンプルでよく見るけど、意味がわからないまま写している…」
こんな経験、ありませんか? デコレータは「なんとなく難しそう」と感じやすい機能ですよね。でも仕組みはシンプルで、一度わかると処理時間の計測・リトライ・計算結果のキャッシュといった「どの関数にも付けたい処理」を、@1行で付け足せるようになります。
この記事は、関数(def)を書いたことがある初〜中級者向けです。5つのサンプルを実際に動かしながら、一緒に見ていきましょう! 😊
- デコレータの正体(「関数を受け取って関数を返す関数」)
- 処理時間を測る
@timerとfunctools.wrapsの役割 - 引数付きのデコレータ
@retry(times=3)の作り方 - 標準ライブラリの
@lru_cacheで計算を速くする方法 - デコレータを重ねたときの「順番」の落とし穴
動作確認は Python 3.13 で行っています。外部ライブラリは使いません。
デコレータとは?「関数に上着を着せる」イメージ

イメージとしては、元の関数はそのままに、外側に「上着」を着せて機能を足すものです。上着を着せても中身(元の関数)は変わりません。でも呼び出すと、上着の部分の処理(前後のprintや時間計測など)も一緒に動きます。
これを支えているのが、Pythonでは関数も「値」として扱えるという性質です。変数に入れたり、別の関数に渡したり、関数から返したりできます。まずはここから確かめてみましょう。
サンプル1:いちばん小さなデコレータ
# 関数は「変数に入れたり、引数に渡したり」できる
def greet(name):
return f"こんにちは、{name}さん"
say = greet # () を付けない → 関数そのものを代入
print(say("田中"))
# 関数を受け取って「前後に処理を足した新しい関数」を返す
def announce(func):
def wrapper(*args, **kwargs):
print(f"--- {func.__name__} を実行します ---")
result = func(*args, **kwargs) # 元の関数を呼ぶ
print(f"--- {func.__name__} が終わりました ---")
return result
return wrapper
@announce # greet2 = announce(greet2) と同じ意味
def greet2(name):
return f"おはよう、{name}さん"
print(greet2("佐藤"))
実行結果はこうなります。
こんにちは、田中さん
--- greet2 を実行します ---
--- greet2 が終わりました ---
おはよう、佐藤さん
ここが重要です👇
say = greetのように ()を付けずに書くと、関数そのものが代入されるannounceは「関数を受け取って、前後にprintを足した新しい関数 wrapper を返す」関数@announceはgreet2 = announce(greet2)の省略記法にすぎない*args, **kwargsで受けると、どんな引数の関数にも付けられる
つまり、デコレータ=関数を受け取って関数を返す関数です。@は「上着を着せる」ための便利な書き方、と覚えておけばOKです。
サンプル2:処理時間を測る @timer と functools.wraps
実用的な例として、関数の実行にかかった時間を表示するデコレータを作ります。「どの処理が遅いのか」を調べるときに、関数の上に1行足すだけで済むのが便利なところです。
import time
from functools import wraps
def timer(func):
@wraps(func) # 元の関数の名前や説明文を引き継ぐ
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[timer] {func.__name__}: {elapsed:.3f}秒")
return result
return wrapper
def timer_no_wraps(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
def make_report(n):
"""1〜nの二乗の合計を求める"""
time.sleep(0.2) # 重い処理の代わり
return sum(i * i for i in range(1, n + 1))
@timer_no_wraps
def make_report2(n):
"""1〜nの二乗の合計を求める"""
return sum(i * i for i in range(1, n + 1))
print("結果:", make_report(1000))
print("wrapsあり:", make_report.__name__, "/", make_report.__doc__)
print("wrapsなし:", make_report2.__name__, "/", make_report2.__doc__)
[timer] make_report: 0.200秒
結果: 333833500
wrapsあり: make_report / 1〜nの二乗の合計を求める
wrapsなし: wrapper / None
ポイントをまとめるとこんな感じです。
- 時間の計測には
time.perf_counter()を使う(短い時間を測るための時計) @wraps(func)を付けないと、関数名がwrapperに、説明文(docstring)がNoneに変わってしまう- エラーメッセージやログに出る関数名がおかしくなるので、自作デコレータには毎回
@wrapsを付けるのがおすすめ
これ、実行せずに答えられますか? ── 「@wraps なしのとき、make_report2.__name__ は何になる?」。答えは上の結果のとおり wrapper です。外側の上着の名前が見えてしまうわけですね。
⚠️ 計測した秒数はパソコンの性能や状況で変わります。ここでは time.sleep(0.2) を入れているので、だいたい0.2秒になります。
サンプル3:引数付きデコレータ @retry(times=3)
ネットワーク通信のように「たまに失敗するけど、もう一度やれば成功する」処理はよくありますよね。そこで、失敗したら少し待って、決めた回数まで試し直すデコレータを作ります。
@retry(times=3, delay=0.1) のようにデコレータに引数を渡したいときは、関数を3段重ねにします。
import time
from functools import wraps
def retry(times=3, delay=0.5):
"""失敗したら delay 秒待って、最大 times 回まで試すデコレータ"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ConnectionError as e:
print(f"[retry] {attempt}回目失敗: {e}")
if attempt == times:
raise # 最後まで失敗したらエラーをそのまま投げる
time.sleep(delay)
return wrapper
return decorator
call_count = 0
@retry(times=3, delay=0.1)
def fetch_price():
"""2回目までは通信エラーになる、不安定な処理のまね"""
global call_count
call_count += 1
if call_count < 3:
raise ConnectionError("サーバーに接続できません")
return 1280
print("取得した価格:", fetch_price())
@retry(times=2, delay=0.1)
def always_fail():
raise ConnectionError("ずっとつながりません")
try:
always_fail()
except ConnectionError as e:
print("あきらめました:", e)

ここが重要です👇
retry(times, delay)→ 設定を受け取ってデコレータを返すdecorator(func)→ 関数を受け取ってwrapperを返す(サンプル2と同じ形)wrapper→ 実際に呼ばれる関数。forとtryで試し直す- キャッチするのは
ConnectionErrorだけ。タイプミスなどのバグまで試し直すと、原因が見えにくくなるからです - 最後まで失敗したら
raiseでエラーをそのまま呼び出し元に返す(失敗を黙って握りつぶさない)
実行すると、1つ目の fetch_price() は2回失敗したあと3回目で価格を返し、2つ目の always_fail() は2回試してあきらめています(下の実行結果画像)。
3段重ねは最初ややこしく感じますが、「外側の関数は設定を覚えておくための箱」と考えると、中の2段はサンプル2と同じだとわかるはずです。
サンプル4:標準ライブラリの @lru_cache で計算を速くする
デコレータは自作するだけでなく、標準ライブラリにも便利なものが入っています。代表例が functools.lru_cache です。一度計算した答えを覚えておき、同じ引数で呼ばれたら計算せずに返す(メモ化)デコレータです。
題材は「n段の階段を、1段か2段ずつ登る方法は何通り?」という問題です。n段目に着く直前は「n-1段目」か「n-2段目」のどちらかなので、stairs(n-1) + stairs(n-2) で求められます。
import time
from functools import lru_cache
calls = 0
def stairs(n):
"""n段の階段を1段か2段ずつ登る方法は何通り?"""
global calls
calls += 1
if n <= 1:
return 1
return stairs(n - 1) + stairs(n - 2)
cached_calls = 0
@lru_cache(maxsize=None) # 一度計算した答えを覚えておく
def stairs_cached(n):
global cached_calls
cached_calls += 1
if n <= 1:
return 1
return stairs_cached(n - 1) + stairs_cached(n - 2)
start = time.perf_counter()
print("キャッシュなし:", stairs(30), "通り /", f"{calls:,}回呼び出し",
f"/ {time.perf_counter() - start:.3f}秒")
start = time.perf_counter()
print("キャッシュあり:", stairs_cached(30), "通り /", f"{cached_calls:,}回呼び出し",
f"/ {time.perf_counter() - start:.5f}秒")
print(stairs_cached.cache_info())

結果を見ると、答えはどちらも 1,346,269通りで同じです。ところが関数の呼び出し回数は、
- キャッシュなし:2,692,537回
- キャッシュあり:31回
と、桁違いの差がつきました。キャッシュなしでは stairs(28) や stairs(27) などを何度も計算し直しているからです。cache_info() の hits=28 は「覚えていた答えを28回使い回した」、misses=31 は「実際に計算したのは31回」という意味です。
⚠️ 注意点もあります。
- 引数がリスト・辞書など、ハッシュできない型だと使えない(TypeError になる)
- 同じ引数でも結果が変わる関数(現在時刻や乱数を使う関数、ファイルを読む関数など)に付けると、古い結果が返ってしまう
maxsize=Noneは「無制限に覚える」設定。引数の種類が非常に多い関数ではメモリを使いすぎることがあるので、maxsize=128のように上限を決める
なお、Python 3.9以降では @functools.cache という短い書き方もあり、lru_cache(maxsize=None) と同じ動きをします。
サンプル5:デコレータを重ねるときは「順番」に注意
デコレータは何個でも重ねられます。ただし、重ねる順番で動きが変わることがあります。@timer と @retry を組み合わせて確かめてみましょう。
@で重ねたとき、関数に近い(下にある)ものから順に上着を着せます。
- 上に
@timer、下に@retry(...)=timer(retry(...)(task)):timerが外側 - 上に
@retry(...)、下に@timer=retry(...)(timer(task)):retryが外側
比べやすいように、ここでは@を使わず、同じ意味の書き方で2パターン作ります。
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"[timer] {func.__name__}: {time.perf_counter() - start:.2f}秒")
return wrapper
def retry(times=3, delay=0.5):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ConnectionError:
print(f"[retry] {attempt}回目失敗")
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
def make_unstable():
"""2回失敗してから成功する関数を作る"""
count = {"n": 0}
def task():
count["n"] += 1
time.sleep(0.1)
if count["n"] < 3:
raise ConnectionError
return "OK"
return task
# パターンA: timer が外側 → リトライ全体の時間を測る
task_a = timer(retry(times=3, delay=0.5)(make_unstable()))
print("A:", task_a())
print()
# パターンB: retry が外側 → 1回ごとの時間を測る
task_b = retry(times=3, delay=0.5)(timer(make_unstable()))
print("B:", task_b())
[retry] 1回目失敗
[retry] 2回目失敗
[timer] task: 1.30秒
A: OK
[timer] task: 0.10秒
[retry] 1回目失敗
[timer] task: 0.10秒
[retry] 2回目失敗
[timer] task: 0.10秒
B: OK
ポイントをまとめるとこんな感じです。
- パターンA(timerが外側):timerは1回だけ表示され、3回の実行(0.1秒×3)+待ち時間(0.5秒×2)を含めた全体の1.30秒を測っている
- パターンB(retryが外側):timerが試すたびに表示され、1回あたりの0.10秒を測っている
- 「ユーザーが待たされた時間」を知りたいならA、「1回の処理の速さ」を知りたいならB
どちらが正しいというわけではなく、何を測りたいかで順番を決めるのが大事です。なお timer の中で try/finally を使っているので、エラーで終わった回の時間も表示されます。
デコレータを使うときのチェックリスト
| チェック項目 | 理由 |
|---|---|
wrapperの引数は *args, **kwargs |
どんな引数の関数にも付けられるようにする |
元の関数の戻り値を return している |
忘れると、付けた関数が全部 None を返すようになる |
@wraps(func) を付けている |
関数名・説明文が wrapper に置き換わるのを防ぐ |
| 引数付きなら3段重ね | 引数付きで作ったデコレータは、設定がなくても @retry() と () を付けて使う |
| 重ねる順番を意識している | 関数に近いものから順に適用される。測る範囲や試し直す範囲が変わる |
特に「returnの書き忘れ」は初心者がつまずきやすいポイントです。デコレータを付けたとたんに結果が None になったら、まずwrapperの中を確認してみてください。
まとめ
- デコレータは「関数を受け取って、機能を足した関数を返す関数」。
@decoはfunc = deco(func)の省略記法 - 自作するときは
*args, **kwargs・return・@wrapsの3点セットを忘れない - 設定を渡したいときは3段重ね(
@retry(times=3)) - 標準ライブラリの
@lru_cacheで、同じ計算のくり返しを大きく減らせる(今回は約269万回→31回) - 重ねるときは関数に近いものから順に適用。何を測りたいか・何を試し直したいかで順番を決める
「@が出てくると読めない…」が「自分でも作れそう」に変わったなら嬉しいです。まずは普段書いている関数に @timer を付けて、どこに時間がかかっているか測るところから、ぜひ試してみてください! 🚀
参考:functools — 高階関数と呼び出し可能オブジェクトの操作(Python公式ドキュメント)
こちらも読まれています
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




