Pythonのデコレータとは:仕組み、自作の方法、実例
Pythonのデコレータは、関数を受け取り、別の関数(多くはラッパー)を返す関数です。ラッパーは元の関数の前後に処理を加えられます(ログ、実行時間の計測、キャッシュ、権限チェックなど)。@デコレータ名 を def の上に書くのは、「関数 = デコレータ(関数)」の省略形です。ラッパーに元の関数の名前やdocstringを残すには、functools.wraps を使います。
Pythonのデコレータは、関数を受け取り、別の関数を返す仕組みです。返される関数はたいてい元の関数を包むラッパーで、呼び出しの前後に処理を加えられます。例えば、ログの記録、実行時間の計測、結果のキャッシュ、アクセス権のチェックなどです。デコレータは @名前 の形で def の上に書きます。FlaskやFastAPI、pytest、標準ライブラリなど、至るところで使われています。
デコレータを簡単に言うと
プレゼントと包装紙を想像してください。中身(関数)は変わりませんが、外側に、最初に動く層が加わります。デコレータはそのような包装です。関数を呼び出すと、まずラッパーのコードが実行され、ラッパーが元の関数を呼び出し、その結果に何か処理を加えられます。
これが可能なのは、Pythonの関数が普通のオブジェクトだからです。関数は別の関数に渡せ、関数から返せ、関数の中で定義できます。デコレータはこの3つの性質の上に成り立っています。
デコレータの仕組み:@は構文上の省略形
呼び出しの前後にメッセージを表示する、簡単なデコレータの例を見てみましょう。
def log_calls(func):
def wrapper():
print(f"{func.__name__} を呼び出します")
result = func()
print(f"{func.__name__} が終了しました")
return result
return wrapper
@log_calls
def hello():
print("こんにちは!")
hello()
hello を呼び出します
こんにちは!
hello が終了しました
def の上の @log_calls は、関数の定義の後に次の行を書くのと完全に同じです。
def hello():
print("こんにちは!")
hello = log_calls(hello)
これで hello という名前は wrapper を指すようになり、元の関数は クロージャの中に残ります。wrapper は外側の関数の変数 func を覚えているからです。大切な点は、デコレータが適用されるのは def が実行されたときの1回だけ(通常はモジュールの読み込み時)であり、ラッパーは呼び出しのたびに動くことです。
クロージャとデコレータ
クロージャは、外側の関数が終わった後も外側の変数を覚えている入れ子の関数です。クロージャがなければ、デコレータは成り立ちません。wrapper が func にアクセスできるのは、この仕組みのおかげです。クロージャの中に状態を持たせることもできます。例えば、呼び出し回数のカウンターです。
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: {calls} 回目の呼び出し")
return func(*args, **kwargs)
return wrapper
Pythonでデコレータを書く:汎用テンプレート
最初の例には2つの欠点があります。ラッパーが引数を受け取れないことと、元の関数の名前を「奪って」しまうことです。どのデコレータも、この動作するテンプレートから書き始めるのがおすすめです。
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 呼び出し前の処理
result = func(*args, **kwargs)
# 呼び出し後の処理
return result
return wrapper
テンプレートには3つの約束があります。
*args, **kwargs― ラッパーはどんな引数も受け取り、そのまま次に渡します。return result― 結果を返し忘れないでください。そうしないと、デコレートされた関数は常にNoneを返します。@functools.wraps(func)― 元の関数のメタデータをラッパーにコピーします。
実行時間を計測するデコレータの例です。
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} の実行時間: {elapsed:.3f} 秒")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum の実行時間: 0.142 秒
functools.wrapsが必要な理由
wraps を使わないと、デコレートされた関数の名前やdocstringなどの属性が置き換わり、外からは wrapper に見えます。
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""2つの数を足します。"""
return a + b
print(add.__name__, add.__doc__) # wrapper None
@functools.wraps(func) を付けると、ラッパーは元の関数の __name__、__doc__、__module__、__qualname__、__annotations__ を受け継ぎ、__wrapped__ 属性で元の関数を参照できるようになります。これはデバッグやトレースバック、help()、名前で関数を登録するフレームワーク、そして本当のシグネチャを表示する inspect.signature にとって重要です。
引数付きデコレータ
デコレータに設定を渡したいことがあります。例えば、@retry(times=3) や @app.route("/") です。このためには、もう1段、入れ子を増やします。外側の関数はパラメータを受け取り、本物のデコレータを返します。このような構造をデコレータファクトリと呼びます。
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping が3行表示される
呼び出しの順序は次のとおりです。repeat(times=3) は decorator を返し、それが ping に適用されて wrapper を返します。つまり、@repeat(times=3) は ping = repeat(times=3)(ping) と同じです。
実用的な例として、エラー時に再試行するデコレータを見てみましょう。
import functools
import time
def retry(times=3, delay=1.0, exceptions=(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:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
...
1つの関数に複数のデコレータを付ける
デコレータは縦に重ねて書けます。適用は下から上へ行われ、def に最も近いものが最初に適用されます。
@timer
@log_calls
def report():
...
# 同じ意味:
report = timer(log_calls(report))
呼び出し時には、外側の層(timer)が最初に動くため、log_calls の実行時間も含めて計測されます。順序は重要です。例えば、あるデコレータがアクセス権をチェックし、別のデコレータが結果をキャッシュする場合です。
標準のデコレータと組み込みのデコレータ
便利なデコレータの多くは、言語と標準ライブラリに最初から含まれています。
| デコレータ | 役割 |
|---|---|
@property | メソッドを読み取り専用の属性に変える。@x.setter と併用すると、代入を制御できる属性になる |
@staticmethod | self を受け取らないクラスのメソッド。クラスの名前空間にある普通の関数 |
@classmethod | インスタンスではなくクラス cls を受け取る。代替コンストラクタに使う |
@functools.cache、@functools.lru_cache | 引数ごとに結果をキャッシュする |
@functools.wraps | 関数のメタデータをラッパーにコピーする |
@dataclasses.dataclass | クラスに __init__、__repr__、__eq__ を生成する |
@contextlib.contextmanager | ジェネレータから with で使えるコンテキストマネージャを作る |
キャッシュ:functools.cacheとlru_cache
キャッシュ用のデコレータは、引数の組み合わせごとに結果を覚え、同じ呼び出しが来たら計算せずに返します。典型例は再帰で計算するフィボナッチ数です。キャッシュがないと、計算量は指数関数的に増えます。
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache(Python 3.9で追加)は、すべての結果を上限なしで保存します。@functools.lru_cache(maxsize=128) は直近の結果だけを保持し、古いものを追い出します。長時間動くサービスではこちらの方が安全です。統計は fib.cache_info() で確認し、fib.cache_clear() でクリアします。キャッシュされる関数の引数はハッシュ可能である必要があります。リストを引数に渡すと TypeError: unhashable type: 'list' になります。また、キャッシュするのは純粋な関数、つまり同じ引数に対していつも同じ結果を返し、副作用のない関数だけにしましょう。
propertyデコレータの役割
@property を使うと、メソッドを括弧なしの属性のように呼び出せ、代入時の処理も制御できます。
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("残高をマイナスにすることはできません")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethodとclassmethod
class Money:
def __init__(self, sen):
self.sen = sen
@classmethod
def from_yen(cls, yen):
return cls(round(yen * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_yen(12.5)
print(m.sen, Money.is_valid(-1)) # 1250 False
@classmethod を使うと、インスタンスではなくクラスそのものを扱うメソッドになります。from_yen はサブクラスのオブジェクトも正しく作れます。
クラスのデコレータと、デコレータとしてのクラス
デコレータはクラスにも適用できます。クラスを受け取り、同じクラスか変更したクラスを返します。@dataclass はこの仕組みで動いています。簡単な例としては、クラスのレジストリがあります。
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
逆に、クラスをデコレータとして使うこともできます。__call__ メソッドを持つオブジェクトなら何でもよく、状態はインスタンスの属性に保存すると便利です。
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
非同期デコレータ
async def の関数に対しては、ラッパー自身もコルーチンでなければならず、結果を await で待つ必要があります。そうしないと、実行されていないコルーチンのオブジェクトが返ってしまいます。
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"開始 {func.__name__}")
result = await func(*args, **kwargs)
print(f"完了 {func.__name__}")
return result
return wrapper
Pythonでデコレータを使う理由
- 横断的な処理 ― ログ、メトリクス、実行時間の計測を、各関数にコードをコピーせずに追加できます。
- キャッシュ ― コストの高い純粋な計算には
@functools.cacheを使います。 - 検査 ― アクセス権、引数の検証、Webハンドラの認証など。
- 登録 ― FlaskやFastAPIのルート(
@app.get("/"))、pytestのフィクスチャ。 - 信頼性 ― 再試行、タイムアウト、エラー処理。
最大の利点は責任の分離です。関数は自分の仕事に集中し、補助的な振る舞いは1行で追加でき、同じように簡単に取り除けます。
よくあるミス
- ラッパーが結果を返さない ― デコレートされた関数が
Noneを返すようになります。 *args, **kwargsがない ― 引数付きで呼び出すとTypeErrorになります。wrapsを忘れる ―__name__やdocstringが違う値になり、ログが分かりにくくなります。- ファクトリの括弧を忘れる ―
@repeat(3)ではなく@repeatと書くと、関数そのものがtimesに渡されてしまいます。 - デコレータの本体に重い処理を書く ― 本体はインポート時に実行され、呼び出し時には実行されません。
まとめ
Pythonのデコレータは高階関数です。関数を受け取り、ラッパーを返します。@ は代入を短く書くための記法にすぎません。まず *args, **kwargs、return result、functools.wraps を含むテンプレートから始め、設定が必要になったらファクトリの層を加えましょう。デコレータを支える関数、クロージャ、型注釈は、講座の実践で扱っています。
よくある質問
デコレータを取り外すことはできますか?
functools.wraps を使っていれば、元の関数は __wrapped__ 属性で参照できます。func.__wrapped__(...) を呼ぶと、ラッパーを通さずに元の関数が実行されます。そうでなければ、@ の行をコードから削除するしかありません。
デコレータのコードはいつ実行されますか?
デコレータの本体は、関数の定義時に一度だけ実行されます。通常はモジュールのインポート時です。wrapper の中のコードは、デコレートされた関数が呼び出されるたびに実行されます。
クラスのメソッドにもデコレータを付けられますか?
はい。*args, **kwargs を使う通常のデコレータはメソッドでも動きます。self は単に args の最初の要素になります。
デコレータと継承の違いは何ですか?
継承はクラスの階層を通じて振る舞いを変えます。一方デコレータは、特定の関数やクラスを外側から包みます。コードを変えずに、他のデコレータと組み合わせられる点が特徴です。