ProgrammingPython開発者

Pythonの引数付きデコレーターとは何ですか?どのように実装され、どこでその適用が正当化され、引数付きデコレーターを自作する際に重要な注意点は何ですか?

Hintsage AIアシスタントで面接を突破

解答

背景

デコレーターは、関数やメソッドの動作を変更できる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回)

主な特徴:

  • 引数付きのデコレーターは常に三重の関数のネストを通じて実装されます
  • ラッパーは結果を返し、*args/**kwargsを正しく透過させる必要があります
  • メタデータを保存するためにfunctools.wrapsを忘れないでください

すり抜ける質問。

通常のデコレーターと同様に引数付きデコレーターを実装できないのはなぜですか?

@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

よくあるエラーとアンチパターン

  • 三重の関数のネストが必要であることを忘れ、2つ(あるいは1つ)だけ作成するため、結果はデコレーターになりません
  • *args/**kwargsをラッパーの中に透過させない
  • functools.wrapsがないため、関数のメタ情報を失う

実生活の例

ネガティブケース

引数やネストされた関数の数を考慮せずにデコレーターを実装しました:

def log(level): def wrapper(func): # エラー — ラッパーはより深くあるべき print(f"Log: {level}") func() # 関数はデコレーターとして返されません return wrapper @log("INFO") def action(): print("Work!")

利点:

  • シンプルに見える

欠点:

  • デコレーターが機能せず、関数はaction()の呼び出し時ではなくデコレーション時に呼び出されます

ポジティブケース

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()

利点:

  • 正しい構造で、拡張がしやすく、クリーンなログ

欠点:

  • 特に初心者にとっては読みづらい