デコレータ(decorator)は、関数を包んで、その前後に処理を追加する仕組みです。@記号を関数の上に書くだけで、元の関数のコードを変えずに、ログ出力・実行時間の計測・アクセス制御といった機能を足せます。FlaskやFastAPIなどのフレームワークでも@app.routeのような形で多用される、Pythonらしい便利な機能です。
つまずきやすいのは、引数のある関数に対応するための*args・**kwargs、そしてfunctools.wrapsを付けないと元の関数の名前(__name__)が失われることです。さらに、複数のデコレータを重ねたときの実行順も間違えやすいポイントです。この記事では、実機のPythonで確認しながら、デコレータを整理します。
- デコレータは関数を受け取り、包んだ関数を返す関数です。
@デコレータ名で適用します。 - 中の
wrapperは*args, **kwargsで引数を受け取り、元の関数にそのまま渡します。 - 元の関数の戻り値は、
wrapperでreturnして返します。 @functools.wraps(func)を付けると、元の関数の名前やドキュメントが保たれます。- 複数重ねると、関数に近いほうが先に適用され、外側が後から包みます。
- 引数を取るデコレータは、デコレータを返す関数として3段階で書きます。
デコレータの土台になる関数(def)の使い方、メッセージ整形に使うf-string、繰り返し処理のenumerate/zipもあわせて参考になります。
デコレータとは(関数を包む)
Pythonでは関数も「値」として扱え、関数を引数に取ったり、関数を返したりする関数が書けます。デコレータはこれを利用して、元の関数を別の関数(wrapper)で包む仕組みです。包むことで、元の処理の前後に好きな処理を挟めます。
基本のデコレータ
もっとも基本的なデコレータを見てみます。funcを受け取り、その前後に処理を足したwrapperを返します。使うときは関数の上に@デコレータ名を書きます。
def my_decorator(func):
def wrapper():
print("処理の前")
func() # 元の関数を実行
print("処理の後")
return wrapper
@my_decorator
def hello():
print("こんにちは")
hello()
# 処理の前
# こんにちは
# 処理の後
@my_decoratorを付けたhelloを呼ぶと、処理の前→こんにちは→処理の後の順で実行されました。@my_decoratorは、実はhello = my_decorator(hello)を書いたのと同じです。helloという名前が、my_decoratorが返したwrapperに置き換わるイメージです。これにより、hello本体のコードを一切変えずに、前後の処理を追加できます。
引数のある関数に対応する
実際の関数は引数や戻り値を持ちます。どんな関数にも対応できるように、wrapperは*args, **kwargsで引数を受け取り、それを元の関数にそのまま渡し、戻り値をreturnで返します。
def my_decorator(func):
def wrapper(*args, **kwargs): # どんな引数でも受け取れる
print("前")
result = func(*args, **kwargs) # そのまま元の関数へ渡す
print("後")
return result # 戻り値を返す
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(2, 3))
# 前
# 後
# 5
実機で確認したところ、wrapper(*args, **kwargs)とし、func(*args, **kwargs)で渡すことで、引数のある関数(add(2, 3))も正しく動き、戻り値5も受け取れました。*argsは位置引数をタプルで、**kwargsはキーワード引数を辞書でまとめて受け取る仕組みです。これを使うと、引数の数や種類が違うどんな関数にも対応できるデコレータになります。逆に、wrapper()のように引数を受け取らないと、引数のある関数に付けたときにエラーになります。また、result = func(...)の戻り値をreturnし忘れると、デコレータを付けた関数の戻り値がNoneになってしまうので注意してください。デコレータのテンプレートとして、この*args, **kwargs+returnの形を覚えておくと便利です。
functools.wrapsを付ける
注意したいのが、デコレータを付けると元の関数の名前(__name__)やドキュメントが失われることです。helloのはずがwrapperになってしまいます。これを防ぐのがfunctools.wrapsです。
import functools
def my_decorator(func):
@functools.wraps(func) # これを付ける
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def hello():
"""あいさつする関数"""
print("こんにちは")
print(hello.__name__) # wraps なし → wrapper / wraps あり → hello
実機で確認したところ、functools.wrapsを付けないデコレータでは、hello.__name__が本来のhelloではなくwrapperになってしまいました。functools.wraps(func)をwrapperの上に付けると、__name__が正しくhelloに保たれます。名前がwrapperになると、デバッグ時に「どの関数か分からない」「ドキュメント生成ツールが正しく動かない」といった問題が起こります。デコレータを書くときは、wrapperの上に@functools.wraps(func)を付けるのがお約束です。元の関数のドキュメント文字列(docstring)も一緒に保たれるため、付けておいて損はありません。
複数デコレータのスタック(順番)
デコレータは複数重ねて使えます。このとき、関数に近いほうが先に適用され、外側が後から包みます。実行されたときの順番は、外側から入って外側で終わる「玉ねぎ」のような形になります。
def deco_a(func):
def wrapper(*a, **k):
print("A開始"); r = func(*a, **k); print("A終了"); return r
return wrapper
def deco_b(func):
def wrapper(*a, **k):
print("B開始"); r = func(*a, **k); print("B終了"); return r
return wrapper
@deco_a # 外側(後から包む)
@deco_b # 内側(関数に近い・先に適用)
def greet():
print("本体")
greet()
# A開始
# B開始
# 本体
# B終了
# A終了
実機でも、@deco_a・@deco_bの順で重ねたgreet()を実行すると、A開始→B開始→本体→B終了→A終了の順になりました。関数にもっとも近い@deco_bが先に関数を包み、その外側を@deco_aが包みます。実行時は外側のdeco_aから入り、内側のdeco_bを通って本体に到達し、戻りは内側から外側へ抜けていきます。「上に書いたデコレータほど外側」と覚えておくと、順番を間違えません。
引数を取るデコレータ
@repeat(3)のようにデコレータ自身に引数を渡したいこともあります。その場合は、「引数を受け取ってデコレータを返す関数」という3段階の構造にします。少し複雑ですが、形は決まっています。
import functools
def repeat(times): # ① 引数を受け取る
def decorator(func): # ② デコレータ本体
@functools.wraps(func)
def wrapper(*args, **kwargs): # ③ 包む関数
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say():
print("やあ")
say()
# やあ
# やあ
# やあ
実機でも、@repeat(3)を付けたsay()が3回実行されました。引数付きデコレータは「repeat(times)がdecoratorを返し、decorator(func)がwrapperを返す」という3重の入れ子になります。@repeat(3)と書くと、まずrepeat(3)が呼ばれてdecoratorが得られ、それがsayに適用される、という流れです。最初は複雑に見えますが、「外側に引数用の関数をもう1つ被せる」と考えると理解しやすくなります。
主なポイントまとめ
デコレータの要点をまとめます。
| 項目 | ポイント |
|---|---|
| 適用 | @デコレータ名を関数の上に書く |
| 引数の受け渡し | wrapper(*args, **kwargs) → func(*args, **kwargs) |
| 戻り値 | return func(...)を忘れない |
| 名前を保つ | @functools.wraps(func)を付ける |
| 重ねる順 | 上に書くほど外側(後から包む) |
| 引数付き | デコレータを返す関数として3段階で書く |
よくある失敗
wrapperで引数を受け取らない
引数のある関数に付けるとエラーです。wrapper(*args, **kwargs)とします。
戻り値をreturnし忘れる
デコレータを付けた関数の戻り値がNoneになります。return func(...)します。
functools.wrapsを付けない
関数名がwrapperになります。@functools.wraps(func)を付けます。
複数デコレータの順番を逆に考える
上に書いたものが外側です。実行は外側から入って外側で終わります。
引数付きデコレータを2段階で書く
引数を取るには3段階必要です。引数用の関数をもう1つ外側に被せます。
よくある質問
@デコレータ名を関数の上に書くだけで、元の関数のコードを変えずにログ出力や計測などの機能を足せます。@my_decoはfunc = my_deco(func)と書くのと同じ意味です。wrapperをwrapper(*args, **kwargs)として、func(*args, **kwargs)で元の関数に引数をそのまま渡します。これで引数の数や種類が違うどんな関数にも対応できます。戻り値もreturn func(...)で返すのを忘れないでください。__name__)やドキュメントがwrapperのものに置き換わってしまいます。@functools.wraps(func)をwrapperの上に付けると、元の関数の名前やドキュメントが保たれます。デバッグやドキュメント生成のために、付けるのが基本です。repeat(times)がdecoratorを返し、decorator(func)がwrapperを返す形です。通常のデコレータの外側に、引数を受け取る関数をもう1つ被せると考えると理解しやすいです。まとめ
- デコレータは関数を包んで前後に処理を足す仕組み。
@名前で適用します。 wrapper(*args, **kwargs)+return func(*args, **kwargs)がテンプレートです。@functools.wraps(func)で元の関数の名前を保ちます。- 複数重ねると上に書くほど外側。実行は外側から入って外側で終わります。
- 引数付きデコレータはデコレータを返す関数として3段階で書きます。
デコレータは最初こそ複雑に見えますが、*args, **kwargs+return+functools.wrapsという「型」を覚えてしまえば、あとは応用です。ログ・計測・キャッシュ・認証など、共通処理を関数から切り出して再利用する強力な手段になります。まずは基本のデコレータから書いて、少しずつ慣れていきましょう。

