Python & AI Tutorials Logo
Python プログラミング

38. デコレータ: 関数に振る舞いを追加する

デコレータ(decorator)は、クリーンで再利用可能なコードを書くための Python の最も強力な機能の1つです。デコレータを使うと、関数の実際のコードを変更せずに、その振る舞いを変更または強化できます。この章では、第23章で学んだファーストクラス関数(first-class functions)とクロージャ(closure)の理解を土台にして、デコレータがどのように動作し、どのように効果的に使うかを学びます。

38.1) デコレータとは何か、なぜ役に立つのか

デコレータ(decorator)とは、別の関数を入力として受け取り、その関数の変更版を返す関数のことです。これは、第23章で学んだように、Python の関数(function)がファーストクラスオブジェクトであり、引数として渡したり、他の関数から返したりできるからです。デコレータを使うと、既存の関数の周囲に追加の振る舞いを「ラップ」できるため、コアロジックを散らかさずに、ログ出力(logging)、時間計測(timing)、検証(validation)、アクセス制御(access control)といった共通機能を簡単に追加できます。

デコレータが重要な理由

たとえば、プログラムに複数の関数があり、それぞれが呼び出されたタイミングをログに記録したいとします。デコレータがない場合、次のように書くかもしれません:

python
# デコレータなし - 重複したログコード
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

この方法には、いくつか問題があります:

  1. コードの重複: ログ出力の行がすべての関数で繰り返されています
  2. 関心事の混在: ログ用コードがビジネスロジックと混ざっています
  3. 保守が大変: ログ形式を変えたい場合、すべての関数を更新しなければなりません
  4. 忘れやすい: 新しい関数にログを入れ忘れるかもしれません

デコレータは、ログ出力の振る舞いをコア関数から分離できるため、これらの問題を解決します:

python
# デコレータあり - クリーンで保守しやすい
# (この章で @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 は関数オブジェクトを作成し、それを名前に束縛します:

python
def greet(name):
    return f"Hello, {name}!"
 
# 関数オブジェクトは別の変数に代入できます
say_hello = greet
 
# 両方の名前は同じ関数オブジェクトを参照します
print(greet("Alice"))      # Output: Hello, Alice!
print(say_hello("Bob"))    # Output: Hello, Bob!

greetsay_hello という名前は、どちらも同じ関数オブジェクトを参照します。これはデコレータがどのように動くかの基本です。

関数は引数として渡せる

関数は、他の値と同様に他の関数へ渡せます:

python
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_twiceadd_five 関数を引数として受け取り、それを2回呼び出しています。

関数は他の関数を返せる

関数は、新しい関数を作って返すことができます:

python
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: 50

make_multiplier 関数は、新しい関数を返します。この関数は、第23章で学んだクロージャ(closure)を通して factor の値を「覚えています」。

関数をラップする: コアとなるデコレータパターン

デコレータパターンは、これらの概念を組み合わせたものです。関数を入力に取り、振る舞いを追加するラッパー関数(wrapper function)を作成し、そのラッパーを返します:

python
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

何が起きているか追ってみましょう:

  1. simple_wrappersay_hellooriginal_func として受け取る
  2. 次のことをする新しい関数 wrapper を作る:
    • "Before calling the function" を表示する
    • original_func()(つまり say_hello)を呼び出す
    • "After calling the function" を表示する
    • 結果を返す
  3. simple_wrapperwrapper 関数を返す
  4. wrapped_hello() を呼ぶと、実際には wrapper を呼んでおり、その中で元の say_hello が呼び出される

これが、すべてのデコレータの背後にある中核パターンです。

引数を取る関数の扱い

上のラッパーは引数を取らない関数でしか動きません。どんな関数でも動くようにするには *args**kwargs が必要です:

python
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) を呼ぶと、それらを元の関数の引数として展開します

このパターンにより、引数の数に関係なく、どんな関数でもラッパーが動作します。

よりクリーンな構文へ

このパターンがデコレータの土台です。次に学ぶデコレータ構文は、このパターンをよりクリーンに適用する方法にすぎません。次のように書く代わりに:

python
greet = flexible_wrapper(greet)

@ 構文を使います:

python
@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 を置けます:

python
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 構文は、次の記述と完全に等価です:

python
def calculate_total(prices):
    return sum(prices)
 
calculate_total = log_call(calculate_total)

しかし @ 構文のほうがずっとクリーンで、その関数がデコレートされていることがすぐにわかります。

複数のデコレータを重ねる

同じ関数に複数のデコレータをスタックして適用できます:

python
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

デコレータをスタックした場合、適用は 下から上へ(関数に近いほうが先)行われます:

python
@timer          # 2番目に適用(最外層)
@log_call       # 1番目に適用(関数に最も近い)
def process_data(items):
    pass

これは次と等価です:

python
process_data = timer(log_call(process_data))

適用順序(下から上):

  1. @log_call が最初に元の関数をラップする
  2. @timer がその結果をラップする(すでにラップされた関数をさらにラップする)

実行順序(上から下、最外層から最内層):

  1. timer の wrapper が開始(最外層、最初に実行)
  2. log_call の wrapper が開始(内側の wrapper)
  3. 元の関数が実行
  4. log_call の wrapper が終了
  5. timer の wrapper が終了(最外層、最後に終了)

デコレータは包装紙の層のようなものだと考えてください。内側から貼っていきますが、ほどく(実行する)ときは外側から内側へ進みます。

デコレータの適用:

元の関数
process_data

ステップ 1: @log_call(下側のデコレータ)

log_call が元の関数をラップ

ステップ 2: @timer(上側のデコレータ)

timer が log_call の wrapper をラップ

最終形: timer が log_call が元の関数をラップ

実行フロー:

process_data を呼び出す

1. timer の wrapper 開始
2. log_call の wrapper 開始
3. 元の関数が実行
4. log_call の wrapper 終了
5. timer の wrapper 終了

結果を返す

38.4) 実用的なデコレータ例(ログ出力、時間計測、検証)

ここからは、実際のプログラムで使うかもしれない実用的なデコレータをいくつか見ていきます。これらの例は一般的なパターンを示し、デコレータが現実の問題をどのように解決するかを示します。

例1: 強化されたログ出力デコレータ

タイムスタンプを含み、例外を扱う、より高度なログ出力デコレータです:

python
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: パフォーマンス時間計測デコレータ

関数の実行時間を測定し、報告するデコレータです:

python
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: 入力検証デコレータ

実行前に関数の引数を検証するデコレータです:

python
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) 基本デコレータ:

この例に基づくと:

python
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 が行うこと:

  1. @log_call を評価 → 結果: log_call 自体(関数オブジェクト)
  2. greet に適用: greet = log_call(greet)

B) デコレータファクトリ:

この例に基づくと:

python
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 が行うこと:

  1. @repeat(3) を評価 → 結果: repeat(3)呼び出され、デコレータ関数を返す
  2. そのデコレータを greet に適用: greet = decorator(greet)

違いは、@log_call は関数そのものを渡しますが、@repeat(3) はデコレータを返す関数(repeat)を 呼び出す という点です。

3つのレベルを理解する

デコレータファクトリには 3 つのネストした関数があり、それぞれ特定の役割を持ちます:

python
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) で何が起きるか追ってみましょう:

python
# 書いたコード:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

ステップ1: Python が repeat(3) を評価する

python
decorator = repeat(3)  # ファクトリがデコレータを返す(times=3 がキャプチャされる)

ステップ2: Python が greet にデコレータを適用する

python
def greet(name):
    print(f"Hello, {name}!")
 
greet = decorator(greet)  # デコレータが wrapper を返す(func=greet がキャプチャされる)

注: この時点で、greet は wrapper 関数を参照するようになります。元の greetfunc にキャプチャされます。

ステップ3: greet("Alice") を呼ぶと wrapper が実行される

python
greet("Alice")  # 実際には wrapper("Alice") を呼ぶ
# wrapper はキャプチャした 'times' と 'func' を使う

なぜ3レベル必要なのか?

各レベルはクロージャ(closure)を通して別々の情報をキャプチャします:

python
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: 設定可能なログ出力デコレータ

ここでは、設定を受け取れるログ出力デコレータの実用例を示します:

python
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: 複数引数を取るデコレータ

ここでは、数値範囲を検証するデコレータを示します:

python
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 を使い続けると、フレームワークやライブラリでデコレータが広く使われていることに気づくでしょう。そして、よくある問題をエレガントに解決するために、自分自身のデコレータを書く機会もたくさん見つかるはずです。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai