背景
デコレーターは、関数やメソッドの動作を変更できるPythonの強力なツールです。時には、単に関数を「ラップ」するのではなく、引数を使用してデコレーターを設定する必要があります。これらのケースは、ロギング、時間のチェック、アクセス制限などの場面で見られます。
問題
通常のデコレーターは、ラップする必要のある1つの関数のみを受け取ります。デコレーター自体に引数を渡す必要がある場合、構文が複雑になり、特に関数のネストや*args/**kwargsの透過でエラーが発生しやすくなります。
解決策
引数付きのデコレーターは、より高次の関数で実装されます。まず外部の「デコレーティング」関数が引数を受け取り、デコレーターを作成して返し、そのデコレーターが関数を受け取ってラッパーを返します:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # 出力: Hello, Python! (3回)
主な特徴:
通常のデコレーターと同様に引数付きデコレーターを実装できないのはなぜですか?
@decoratorを使うと、Pythonは関数をデコレーターの引数として渡します。括弧を追加すると(@decorator())、Pythonはまず関数を呼び出し、その結果がデコレーターとして解釈されます。
def deco(func): # 通常のデコレーター: @deco def deco_with_args(arg): # 引数付きデコレーター: @deco_with_args(arg)
呼び出しレベルで引数付きデコレーターと引数なしデコレーターはどのように異なりますか?
引数なしのデコレーターは関数を受け取りますが、引数付きのデコレーターは関数ではなく引数を受け取り、デコレーターを返します。
functools.wrapsを正しく使用するにはどうすればよいですか?その理由は何ですか?
functools.wraps(func)はラッパー内のオリジナルの関数の名前、ドキュメント文字列、その他のメタデータを保存します。さもなければ、これらの情報がラッパーで置き換えられ、デバッグやイントロスペクションの妨げになります。
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
引数やネストされた関数の数を考慮せずにデコレーターを実装しました:
def log(level): def wrapper(func): # エラー — ラッパーはより深くあるべき print(f"Log: {level}") func() # 関数はデコレーターとして返されません return wrapper @log("INFO") def action(): print("Work!")
利点:
欠点:
functools.wrapsと正しいネストされた関数を使用:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
利点:
欠点: