def デコレータ / @構文
| 対応: | Python 2(2000) |
|---|
デコレータは関数やクラスを受け取り、機能を追加した新しい関数(またはクラス)を返す仕組みです。『@デコレータ名』の構文で関数定義の直前に書くと、関数が定義された時点でデコレータが適用されます。デコレータを自作するときは『functools.wraps()』を使って元の関数のメタデータを保持するために使うことが多いです。
構文
from functools import wraps
# デコレータの定義
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 前処理
result = func(*args, **kwargs)
# 後処理
return result
return wrapper
# デコレータの適用
@my_decorator
def my_function():
pass
# 引数付きデコレータ
def decorator_with_args(arg1, arg2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
構文・関数一覧
| 構文・関数 | 概要 |
|---|---|
| @デコレータ名 | 関数定義の前に書いてデコレータを適用する。 |
| @wraps(func) | wrapperに元のfuncのメタデータ(__name__など)をコピーする。 |
| *args, **kwargs | デコレータ内でラップする関数のすべての引数を受け取るパターン。 |
| @deco1 @deco2 | デコレータのスタック。下から順に適用される(deco2→deco1の順)。 |
サンプルコード
以下は発展的な使い方です。
decorator.py
from functools import wraps
import time
# 実行時間を計測するデコレータ
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[{func.__name__}] 実行時間: {elapsed:.4f}秒")
return result
return wrapper
@timer
def heavy_process(n):
"""重い処理のシミュレーション"""
return sum(range(n))
print(heavy_process(1000000)) # [heavy_process] 実行時間: 0.XXXX秒
# 引数付きデコレータ(繰り返し実行)
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"試行 {i+1}/{times} 失敗: {e}")
raise RuntimeError(f"{func.__name__} が{times}回失敗しました")
return wrapper
return decorator
@retry(times=3)
def unstable_api():
import random
if random.random() < 0.7:
raise ConnectionError("接続エラー")
return "成功"
# デコレータのスタック(複数のデコレータを重ねる)
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"**{func(*args, **kwargs)}**"
return wrapper
def upper(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@bold # 2番目に適用(外側)
@upper # 1番目に適用(内側)
def greet(name):
return f"hello, {name}"
print(greet('world')) # **HELLO, WORLD**(upper → bold の順で適用)
# クラスベースのデコレータ
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
wraps(func)(self) # メタデータをコピー
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
return self.cache[args]
@Memoize
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30)) # 832040(キャッシュで高速)
python3 decorator.py [heavy_process] 実行時間: 0.0059秒 499999500000 **HELLO, WORLD** 832040
実践パターン: Webアプリケーションでよく使うデコレータ
web_decorators.py
from functools import wraps
# ロギングデコレータ(引数と戻り値を記録)
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} called with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
# 認証チェックデコレータ
def require_login(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get('is_authenticated'):
raise PermissionError("ログインが必要です")
return func(user, *args, **kwargs)
return wrapper
# キャッシュデコレータ(シンプルなメモ化)
def cache(func):
store = {}
@wraps(func)
def wrapper(*args):
if args not in store:
store[args] = func(*args)
return store[args]
return wrapper
@log_calls
@require_login
def get_profile(user):
return {"name": "岡部倫太郎", "lab": "未来ガジェット研究所"}
@cache
def expensive_calc(n):
return sum(range(n))
user = {"is_authenticated": True}
get_profile(user)
print(expensive_calc(1000000)) # 初回は計算
print(expensive_calc(1000000)) # 2回目以降はキャッシュから返す
python3 web_decorators.py
[LOG] get_profile called with args=({'is_authenticated': True},), kwargs={}
[LOG] get_profile returned {'name': '岡部倫太郎', 'lab': '未来ガジェット研究所'}
499999500000
499999500000
よくあるミス1: @wraps(func)の省略
@wraps(func) を省略すると関数名やdocstringが失われます。
decorator_wraps_ng.py
from functools import wraps
# NG: @wraps(func) を省略すると関数名やdocstringが失われる
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # wraps なし
@bad_decorator
def greet(name):
"""挨拶をします。"""
return f"Hello, {name}"
print(greet.__name__) # 'wrapper' になってしまいます。
print(greet.__doc__) # None になってしまいます。
python3 decorator_wraps_ng.py wrapper None
decorator_wraps_ok.py
from functools import wraps
# OK: @wraps(func) を必ず付ける
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def greet(name):
"""挨拶をします。"""
return f"Hello, {name}"
print(greet.__name__) # 'greet' になります。
print(greet.__doc__) # '挨拶をします。' になります。
python3 decorator_wraps_ok.py greet 挨拶をします。
よくあるミス2: 引数付きデコレータの括弧忘れ
引数付きデコレータで括弧を忘れると TypeError になります。(retry はサンプルコードセクションで定義した引数付きデコレータです)
decorator_args_ng.py
# NG: 引数付きデコレータで括弧を忘れると TypeError になります。
@retry # NG: retry は引数付きデコレータなので括弧が必要
def my_func():
pass
python3 decorator_args_ng.py Traceback (most recent call last): ... TypeError: wrapper() takes 0 positional arguments but 1 was given
decorator_args_ok.py
from functools import wraps
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if i == times - 1:
raise
print(f"リトライ {i + 1}/{times}: {e}")
return wrapper
return decorator
@retry(times=3)
def stable_func():
print("正常に実行されました")
stable_func()
python3 decorator_args_ok.py 正常に実行されました
概要
デコレータはPythonのシンタックスシュガーで、『@my_decorator』を書くことは『func = my_decorator(func)』と等価です。デコレータを重ねた場合は下から順(内側から外側)に適用されます。
引数付きデコレータは3重の関数定義になりますが、構造を理解すれば複雑ではありません。外側の関数で引数を受け取り、中間の関数でfuncを受け取り、内側のwrapperで実際の処理をラップします。
デコレータはログ出力や権限チェックなど、複数の関数に共通して必要な処理の実装に最適で、コードの重複を避けるDRY原則の実現に役立ちます。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。