38. デコレータ: 関数に振る舞いを追加する
デコレータ(decorator)は、クリーンで再利用可能なコードを書くための Python の最も強力な機能の1つです。デコレータを使うと、関数の実際のコードを変更せずに、その振る舞いを変更または強化できます。この章では、第23章で学んだファーストクラス関数(first-class functions)とクロージャ(closure)の理解を土台にして、デコレータがどのように動作し、どのように効果的に使うかを学びます。
38.1) デコレータとは何か、なぜ役に立つのか
デコレータ(decorator)とは、別の関数を入力として受け取り、その関数の変更版を返す関数のことです。これは、第23章で学んだように、Python の関数(function)がファーストクラスオブジェクトであり、引数として渡したり、他の関数から返したりできるからです。デコレータを使うと、既存の関数の周囲に追加の振る舞いを「ラップ」できるため、コアロジックを散らかさずに、ログ出力(logging)、時間計測(timing)、検証(validation)、アクセス制御(access control)といった共通機能を簡単に追加できます。
デコレータが重要な理由
たとえば、プログラムに複数の関数があり、それぞれが呼び出されたタイミングをログに記録したいとします。デコレータがない場合、次のように書くかもしれません:
# デコレータなし - 重複したログコード
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# 関数を使う
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60この方法には、いくつか問題があります:
- コードの重複: ログ出力の行がすべての関数で繰り返されています
- 関心事の混在: ログ用コードがビジネスロジックと混ざっています
- 保守が大変: ログ形式を変えたい場合、すべての関数を更新しなければなりません
- 忘れやすい: 新しい関数にログを入れ忘れるかもしれません
デコレータは、ログ出力の振る舞いをコア関数から分離できるため、これらの問題を解決します:
# デコレータあり - クリーンで保守しやすい
# (この章で @log_calls の作り方を学びます)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# 関数を使うと同じ出力になります
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60何が違うのでしょうか? ログ出力の振る舞いは @log_calls デコレータ内で一度だけ定義され、どこでも再利用されます。コア関数はクリーンなまま、本来の目的に集中できます。
デコレータのよくある用途
デコレータは特に次の用途で役立ちます:
- ログ出力(logging): 関数がいつ呼び出され、何を返したかを記録する
- 時間計測(timing): 関数の実行にかかった時間を測定する
- 検証(validation): 関数引数が特定の要件を満たすか確認する
- キャッシュ(caching): 高コストな関数呼び出しの結果を保存して再利用する
- アクセス制御(access control): 関数実行を許可する前に権限を確認する
- リトライロジック(retry logic): 失敗した操作を自動的に再試行する
- 型チェック(type checking): 引数と戻り値の型を検証する
重要な利点は、デコレータを一度書けば、1行のコードで多くの関数に適用できることです。
38.2) オブジェクトとしての関数: デコレータの基盤
デコレータを理解する前に、Python において関数(function)がファーストクラスオブジェクト(first-class objects)であるという概念を復習し、さらに掘り下げる必要があります。第23章で学んだように、これは関数が変数に代入でき、引数として渡せて、他の関数から返せることを意味します。
関数は変数に代入できる
関数を定義すると、Python は関数オブジェクトを作成し、それを名前に束縛します:
def greet(name):
return f"Hello, {name}!"
# 関数オブジェクトは別の変数に代入できます
say_hello = greet
# 両方の名前は同じ関数オブジェクトを参照します
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!greet と say_hello という名前は、どちらも同じ関数オブジェクトを参照します。これはデコレータがどのように動くかの基本です。
関数は引数として渡せる
関数は、他の値と同様に他の関数へ渡せます:
def apply_twice(func, value):
"""Apply a function to a value twice."""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)ここでは、apply_twice が add_five 関数を引数として受け取り、それを2回呼び出しています。
関数は他の関数を返せる
関数は、新しい関数を作って返すことができます:
def make_multiplier(factor):
"""Create a function that multiplies by a specific factor."""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50make_multiplier 関数は、新しい関数を返します。この関数は、第23章で学んだクロージャ(closure)を通して factor の値を「覚えています」。
関数をラップする: コアとなるデコレータパターン
デコレータパターンは、これらの概念を組み合わせたものです。関数を入力に取り、振る舞いを追加するラッパー関数(wrapper function)を作成し、そのラッパーを返します:
def simple_wrapper(original_func):
"""Wrap a function with additional behavior."""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# 関数を手動でラップする
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greeting何が起きているか追ってみましょう:
simple_wrapperがsay_helloをoriginal_funcとして受け取る- 次のことをする新しい関数
wrapperを作る:- "Before calling the function" を表示する
original_func()(つまりsay_hello)を呼び出す- "After calling the function" を表示する
- 結果を返す
simple_wrapperがwrapper関数を返すwrapped_hello()を呼ぶと、実際にはwrapperを呼んでおり、その中で元のsay_helloが呼び出される
これが、すべてのデコレータの背後にある中核パターンです。
引数を取る関数の扱い
上のラッパーは引数を取らない関数でしか動きません。どんな関数でも動くようにするには *args と **kwargs が必要です:
def flexible_wrapper(original_func):
"""Wrap a function that can accept any arguments."""
def wrapper(*args, **kwargs):
# *args は位置引数をキャプチャします
# **kwargs はキーワード引数をキャプチャします
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# 関数を手動でラップする
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!*args と **kwargs の動き:
第20章で学んだように、*args と **kwargs を使うと、関数は可変個の引数を受け取れます:
*argsはすべての位置引数をタプルにまとめます**kwargsはすべてのキーワード引数を辞書にまとめますoriginal_func(*args, **kwargs)を呼ぶと、それらを元の関数の引数として展開します
このパターンにより、引数の数に関係なく、どんな関数でもラッパーが動作します。
よりクリーンな構文へ
このパターンがデコレータの土台です。次に学ぶデコレータ構文は、このパターンをよりクリーンに適用する方法にすぎません。次のように書く代わりに:
greet = flexible_wrapper(greet)@ 構文を使います:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"どちらもまったく同じことをしています。@ 構文は、コードをよりクリーンで読みやすくするためのシンタックスシュガー(syntactic sugar)です。
38.3) @decorator 構文: よりクリーンな適用
function_name = decorator(function_name) と書く方法でも動きますが、冗長で忘れやすいです。Python はデコレータをよりクリーンに適用するために @decorator 構文を提供しています。
@ 記号を使う
関数を手動でラップする代わりに、関数定義の直前の行に @decorator_name を置けます:
def log_call(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# デコレートされた関数を使う
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0@log_call 構文は、次の記述と完全に等価です:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)しかし @ 構文のほうがずっとクリーンで、その関数がデコレートされていることがすぐにわかります。
複数のデコレータを重ねる
同じ関数に複数のデコレータをスタックして適用できます:
import time
def log_call(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""Decorator that times function execution."""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30デコレータをスタックした場合、適用は 下から上へ(関数に近いほうが先)行われます:
@timer # 2番目に適用(最外層)
@log_call # 1番目に適用(関数に最も近い)
def process_data(items):
passこれは次と等価です:
process_data = timer(log_call(process_data))適用順序(下から上):
@log_callが最初に元の関数をラップする@timerがその結果をラップする(すでにラップされた関数をさらにラップする)
実行順序(上から下、最外層から最内層):
timerの wrapper が開始(最外層、最初に実行)log_callの wrapper が開始(内側の wrapper)- 元の関数が実行
log_callの wrapper が終了timerの wrapper が終了(最外層、最後に終了)
デコレータは包装紙の層のようなものだと考えてください。内側から貼っていきますが、ほどく(実行する)ときは外側から内側へ進みます。
デコレータの適用:
実行フロー:
38.4) 実用的なデコレータ例(ログ出力、時間計測、検証)
ここからは、実際のプログラムで使うかもしれない実用的なデコレータをいくつか見ていきます。これらの例は一般的なパターンを示し、デコレータが現実の問題をどのように解決するかを示します。
例1: 強化されたログ出力デコレータ
タイムスタンプを含み、例外を扱う、より高度なログ出力デコレータです:
import time
def log_with_timestamp(func):
"""Decorator that logs function calls with timestamps."""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# 処理をシミュレートする
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# 成功する実行をテストする
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# 検証を伴う成功する実行をテストする
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# 例外処理をテストする
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user IDこのデコレータは次のことを行います:
- すべてのログメッセージにタイムスタンプを追加する
- 成功終了と例外の両方をログに残す
- 例外をログに残した後に再送出する(引数なしの
raiseを使用) try/exceptブロックを使って任意の例外を捕捉し、ログに残す
例2: パフォーマンス時間計測デコレータ
関数の実行時間を測定し、報告するデコレータです:
import time
def measure_time(func):
"""Decorator that measures and reports execution time."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# 時間を適切にフォーマットする
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""Find all prime numbers up to limit."""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""Calculate factorial of n."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# デコレートされた関数をテストする
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digitsこのデコレータは、実行時間の長さに応じて(マイクロ秒、ミリ秒、秒)、時間の測定結果を自動的に適切な形式へ整形します。
例3: 入力検証デコレータ
実行前に関数の引数を検証するデコレータです:
def validate_positive(func):
"""Decorator that ensures all numeric arguments are positive."""
def wrapper(*args, **kwargs):
# 位置引数をチェックする
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# キーワード引数をチェックする
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""Calculate area of a rectangle."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""Calculate discounted price."""
discount = price * (discount_percent / 100)
return price - discount
# 正しい入力をテストする
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# 不正な入力をテストする
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10このデコレータは次のことを行います:
- 数値の引数(位置引数とキーワード引数の両方)をすべてチェックする
- 正でないものがあれば説明的なエラーを送出する
- どの引数が検証に失敗したかがわかる明確なエラーメッセージを提供する
38.5) (任意)引数付きデコレータ
これまでのデコレータは、関数を入力として受け取る単純な関数でした。しかし、デコレータの振る舞いを設定したい場合はどうでしょうか? たとえば、リトライデコレータで試行回数を指定したり、ログ出力デコレータでログレベルを指定したりしたいかもしれません。
引数付きデコレータ(decorators with arguments)には、関数のネストをもう1段追加する必要があります。デコレータが「関数を受け取る関数」ではなく、「引数を受け取り、デコレータを返す関数」になります。
パターン: デコレータファクトリ
引数付きデコレータは、実際には デコレータファクトリ(decorator factory)です。つまり、デコレータを作成して返す関数です。これを理解する鍵は、Python が @ 記号をどのように扱うかを知ることです。
重要な原則: Python はまず @ を評価する
Python は @ の後ろにあるものを常に先に評価し、その結果を使って関数をデコレートします。
比較してみましょう:
A) 基本デコレータ:
この例に基づくと:
def log_call(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"Python が行うこと:
@log_callを評価 → 結果:log_call自体(関数オブジェクト)greetに適用:greet = log_call(greet)
B) デコレータファクトリ:
この例に基づくと:
def repeat(times):
"""Level 1: Factory - receives configuration"""
def decorator(func):
"""Level 2: Decorator - receives the function to decorate"""
def wrapper(*args, **kwargs):
"""Level 3: Wrapper - executes when decorated function is called"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Python が行うこと:
@repeat(3)を評価 → 結果:repeat(3)が 呼び出され、デコレータ関数を返す- そのデコレータを
greetに適用:greet = decorator(greet)
違いは、@log_call は関数そのものを渡しますが、@repeat(3) はデコレータを返す関数(repeat)を 呼び出す という点です。
3つのレベルを理解する
デコレータファクトリには 3 つのネストした関数があり、それぞれ特定の役割を持ちます:
def repeat(times): # Level 1: Factory
def decorator(func): # Level 2: Decorator
def wrapper(*args, **kwargs): # Level 3: Wrapper
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorレベル1 - ファクトリ(repeat):
- 受け取るもの: 設定(
times) - 返すもの: デコレータ関数
- 呼ばれるタイミング: Python が
@repeat(3)を評価するとき
レベル2 - デコレータ(decorator):
- 受け取るもの: デコレートする関数(
func) - 返すもの: wrapper 関数
- 呼ばれるタイミング: レベル1の直後、@ 構文の一部として即時に呼ばれる
レベル3 - ラッパー(wrapper):
- 受け取るもの: 呼び出し時の関数引数(
*args, **kwargs) - 返すもの: 結果
- 呼ばれるタイミング: デコレートされた関数を呼ぶたび
ステップごとの実行
@repeat(3) で何が起きるか追ってみましょう:
# 書いたコード:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")ステップ1: Python が repeat(3) を評価する
decorator = repeat(3) # ファクトリがデコレータを返す(times=3 がキャプチャされる)ステップ2: Python が greet にデコレータを適用する
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # デコレータが wrapper を返す(func=greet がキャプチャされる)注: この時点で、greet は wrapper 関数を参照するようになります。元の greet は func にキャプチャされます。
ステップ3: greet("Alice") を呼ぶと wrapper が実行される
greet("Alice") # 実際には wrapper("Alice") を呼ぶ
# wrapper はキャプチャした 'times' と 'func' を使うなぜ3レベル必要なのか?
各レベルはクロージャ(closure)を通して別々の情報をキャプチャします:
def repeat(times): # Captures: times
def decorator(func): # Captures: func (and remembers times)
def wrapper(*args, **kwargs): # Captures: times, func, and receives args
for i in range(times): # キャプチャした 'times' を使う
result = func(*args, **kwargs) # キャプチャした 'func' と 'args' を使う
return result
return wrapper
return decorator- レベル1 は設定(
times)をキャプチャする - レベル2 はデコレート対象の関数(
func)をキャプチャする - レベル3 は呼び出し時の引数(
args,kwargs)を受け取る
3レベルがないと、設定とデコレート対象の関数の両方を記憶する、設定可能なデコレータを作れません。
例1: 設定可能なログ出力デコレータ
ここでは、設定を受け取れるログ出力デコレータの実用例を示します:
def log_with_prefix(prefix="LOG"):
"""Decorator factory that creates a logging decorator with a custom prefix."""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # デフォルトの prefix を使う
def get_average(numbers):
return sum(numbers) / len(numbers)
# デコレートされた関数をテストする
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0注意点:
@log_with_prefix(prefix="INFO")はカスタム prefix を使います@log_with_prefix()はデフォルトの prefix "LOG" を使います- デフォルトを使う場合でも括弧が必要です
例2: 複数引数を取るデコレータ
ここでは、数値範囲を検証するデコレータを示します:
def validate_range(min_value=None, max_value=None):
"""
Decorator factory that validates numeric arguments are within a range.
Args:
min_value: Minimum allowed value (inclusive)
max_value: Maximum allowed value (inclusive)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# 数値引数をすべてチェックする
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""Calculate percentage."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""Calculate age from birth year."""
return current_year - birth_year
# 正しい入力をテストする
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# 不正な入力をテストする
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0引数付きデコレータを使うべきとき
引数付きデコレータは、次の場合に使います:
- デコレータの振る舞いを設定する必要がある
- 同じデコレータを異なる文脈で異なる動作にしたい
- デコレータをより再利用可能で柔軟にしたい
よくある例には次のものがあります:
- 試行回数や待機時間を設定できるリトライデコレータ
- ログレベルやフォーマットを設定できるログ出力デコレータ
- ルールを設定できる検証デコレータ
- キャッシュサイズや有効期限を設定できるキャッシュデコレータ
- 制限値を設定できるレート制限デコレータ
複雑さに関する注意
引数付きデコレータは、複雑さが1段増します。書くときは:
- 明確で説明的なパラメータ名を使う
- 妥当なデフォルト値を用意する
- パラメータを説明する docstring を含める
- 追加の柔軟性が複雑さに見合うか検討する
単純なケースでは、引数なしのデコレータのほうが明確で、理解しやすいことがよくあります。
デコレータは、クリーンで保守しやすい Python コードを書くための強力なツールです。ログ出力(logging)、時間計測(timing)、検証(validation)のような横断的関心事(cross-cutting concerns)をコアのビジネスロジックから切り離せるため、コードの可読性、テストのしやすさ、変更のしやすさが向上します。Python を使い続けると、フレームワークやライブラリでデコレータが広く使われていることに気づくでしょう。そして、よくある問題をエレガントに解決するために、自分自身のデコレータを書く機会もたくさん見つかるはずです。