Pythonのデコレーターを引数付きで作成する

プログラミング

Pythonのデコレーターを引数付きで作成する

Pythonにおけるデコレーターは、既存の関数やメソッドの振る舞いを、元のコードを変更せずに変更・拡張するための強力な機能です。デコレーターは、関数を引数として受け取り、新しい関数を返す関数です。これにより、ロギング、アクセス制御、パフォーマンス測定などの共通のタスクを、コードの重複なく効率的に実装できます。

デコレーターを引数付きで作成することは、デコレーターにさらに柔軟性を持たせ、よりカスタマイズされた振る舞いを実現するために不可欠です。引数付きデコレーターは、デコレーター自体に設定を渡すことを可能にし、その設定に基づいてデコレーターの動作を制御します。

引数付きデコレーターの基本構造

引数付きデコレーターを作成するには、通常、3つのネストされた関数が必要です。

外側の関数: この関数は、デコレーターに渡される引数を受け取ります。この関数は、デコレーターを適用する対象となる関数を引数として受け取り、ラッパー関数を返します。

中間の関数 (デコレーター自身): この関数は、外側の関数によって返され、デコレーターを適用する対象となる関数を受け取ります。この関数は、ラッパー関数を定義し、それを返します。

内側の関数 (ラッパー関数): この関数は、実際にデコレーターを適用する対象となる関数の実行をラップします。デコレーターに渡された引数と、デコレーターを適用する対象となる関数に渡される引数を受け取ります。この関数内で、元の関数の実行前後の処理を追加したり、条件分岐を入れたりすることができます。

この構造により、デコレーターはまず設定引数を受け取り、次にデコレーターを適用する関数を受け取り、最終的にその関数をラップするロジックを実行します。

コード例

以下に、引数付きデコレーターの基本的な構造を示すコード例を示します。この例では、デコレーターはメッセージ文字列を引数として受け取り、そのメッセージを元の関数の実行前に表示します。

“`python
def decorator_with_args(message):
def decorator(func):
def wrapper(*args, **kwargs):
print(f”Decorator message: {message}”)
return func(*args, **kwargs)
return wrapper
return decorator

@decorator_with_args(“Hello from decorator!”)
def greet(name):
print(f”Hello, {name}!”)

greet(“Alice”)
“`

この例では、`decorator_with_args` が外側の関数であり、`message` を引数として受け取ります。この関数は `decorator` 関数を返します。`decorator` 関数は、デコレーターを適用する対象となる関数 (`func`) を受け取り、`wrapper` 関数を返します。`wrapper` 関数は、元の関数の実行前に `message` を表示し、その後で元の関数 `func` を実行します。

引数付きデコレーターの活用例

引数付きデコレーターは、様々なシナリオで役立ちます。以下にいくつかの代表的な活用例を挙げます。

1. ロギングレベルの制御

デコレーターにログレベルを引数として渡すことで、どのレベルのログを表示するかを制御できます。

“`python
import functools

def log_execution(level=”INFO”):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f”[{level}] Executing {func.__name__} with args: {args}, kwargs: {kwargs}”)
result = func(*args, **kwargs)
print(f”[{level}] {func.__name__} finished with result: {result}”)
return result
return wrapper
return decorator

@log_execution(level=”DEBUG”)
def add(a, b):
return a + b

@log_execution(level=”WARNING”)
def divide(a, b):
return a / b

add(5, 3)
divide(10, 2)
“`

この例では、`log_execution` デコレーターは `level` という引数を受け取ります。これにより、ログメッセージに表示されるレベルを `DEBUG` や `WARNING` など、実行時に指定できます。`@functools.wraps(func)` を使用することで、デコレーターを適用した後の関数のメタ情報(`__name__` など)が保持され、デバッグが容易になります。

2. アクセス制御

特定のユーザーロールや権限を持つユーザーのみが関数を実行できるように、デコレーターに権限レベルを渡すことができます。

“`python
def require_permission(required_role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_user_role = get_current_user_role() # 仮の関数
if current_user_role == required_role:
return func(*args, **kwargs)
else:
raise PermissionError(f”User does not have the required role: {required_role}”)
return wrapper
return decorator

# 仮の関数
def get_current_user_role():
return “admin”

@require_permission(“admin”)
def delete_user(user_id):
print(f”Deleting user {user_id}”)

@require_permission(“editor”)
def edit_post(post_id):
print(f”Editing post {post_id}”)

delete_user(123)
# edit_post(456) # これはPermissionErrorになります
“`

この例では、`require_permission` デコレーターは `required_role` を引数として受け取ります。関数が呼び出される前に、現在のユーザーのロールが `required_role` と一致するかどうかを確認し、一致しない場合は `PermissionError` を発生させます。

3. パフォーマンス測定

デコレーターに閾値を設定し、実行時間がその閾値を超えた場合に警告を出すといった、パフォーマンス監視に利用できます。

“`python
import time

def time_limited(max_time):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
elapsed_time = end_time – start_time
if elapsed_time > max_time:
print(f”Warning: {func.__name__} took {elapsed_time:.4f} seconds, exceeding the limit of {max_time} seconds.”)
return result
return wrapper
return decorator

@time_limited(max_time=0.5) # 0.5秒以上かかったら警告
def slow_function():
time.sleep(1)
print(“Slow function finished.”)

slow_function()
“`

この例では、`time_limited` デコレーターは `max_time` を引数として受け取ります。関数の実行時間を計測し、設定された `max_time` を超えた場合に警告メッセージを表示します。

デコレーターを引数付きで作成する際の注意点

引数付きデコレーターは非常に便利ですが、いくつかの注意点があります。

`functools.wraps` の使用

前述の例でも使用しましたが、デコレーターを定義する際には `functools.wraps` を使用することが強く推奨されます。これは、デコレーターを適用した後の関数が、元の関数のドキュメンテーション文字列 (`__doc__`)、名前 (`__name__`)、その他のメタデータを受け継ぐようにするためです。これを怠ると、デバッグやイントロスペクションが困難になる可能性があります。

ネストの深さと可読性

引数付きデコレーターは3つのネストされた関数が必要となるため、コードが深くなりがちです。デコレーターのロジックが複雑になりすぎると、コードの可読性が低下する可能性があります。デコレーターの目的を明確にし、必要以上に複雑にならないように注意が必要です。

デコレーターの合成

複数のデコレーターを一つの関数に適用する場合、適用順序によって結果が変わることがあります。引数付きデコレーターの場合も同様で、その順序を理解しておくことが重要です。

まとめ

Pythonの引数付きデコレーターは、デコレーターに設定やパラメータを渡すことを可能にし、コードの再利用性と柔軟性を大幅に向上させます。3つのネストされた関数構造を理解し、`functools.wraps` を適切に使用することで、強力で保守性の高いデコレーターを作成できます。ロギング、アクセス制御、パフォーマンス測定など、様々な実用的なシナリオで活用されており、Pythonicなコーディングスタイルを実践する上で不可欠なテクニックと言えるでしょう。