単一の Python 関数に複数のデコレータを使用する方法

PythonBeginner
オンラインで実践に進む

はじめに

Python のデコレータは、関数のソースコードを変更することなく、その振る舞いを変更できる強力なツールです。このチュートリアルでは、単一の Python 関数に複数のデコレータを使用する方法を探り、コードの再利用と関数の拡張に関する無限の可能性を開きます。

Python デコレータの理解

Python のデコレータとは何か?

Python のデコレータは、関数やクラスのソースコードを変更することなく、その振る舞いを変更する強力で柔軟な方法です。デコレータは、関数を別の関数で「ラップ」する方法であり、ラッパー関数が元の関数の呼び出し前および/または呼び出し後にコードを実行できるようにします。

デコレータを使用する理由は何か?

デコレータは、以下のような様々なタスクに役立ちます。

  • 関数呼び出しのロギング
  • 関数の結果のキャッシュ
  • アクセス制御の強制
  • 関数の実行時間の測定
  • 失敗した関数呼び出しの再試行

デコレータはどのように動作するか?

Python のデコレータは、@ 記号に続けてデコレータ関数を記述することで定義されます。デコレータ関数は関数を引数として受け取り、いくつかの追加処理を行った後、元の関数の代わりに呼び出すことができる新しい関数を返します。

以下は、関数に渡された引数をログに記録するデコレータ関数の簡単な例です。

def log_args(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_args
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

これは以下のように出力されます。

Calling add_numbers with args=(2, 3) and kwargs={}
5

ネストされたデコレータ

デコレータはネストすることができ、単一の関数に複数のデコレータを適用することができます。デコレータが適用される順序は重要であり、デコレータ関数が実行される順序を決定します。

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

これは以下のように出力されます。

!XEbal,OLLEH

デコレータの引数

デコレータは引数を取ることもでき、これによりデコレータの振る舞いをカスタマイズすることができます。これは、さまざまな方法で設定できるデコレータを作成したい場合に便利です。

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = ""
            for _ in range(n):
                result += func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

これは以下のように出力されます。

Hello, LabEx!Hello, LabEx!Hello, LabEx!

複数のデコレータの適用

デコレータの適用順序

関数に複数のデコレータを適用する場合、その適用順序は重要です。デコレータは下から上に適用されます。つまり、最も内側のデコレータが最初に適用され、最も外側のデコレータが最後に適用されます。

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

これは以下のように出力されます。

!XEBAL,OLLEH

デコレータの積み重ね

単一の関数に対して、デコレータを順番に適用することで複数のデコレータを積み重ねることもできます。これはデコレータをネストするのと同等ですが、コードの可読性を向上させることができます。

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

def greet(name):
    return f"Hello, {name}!"

greet_upper_reverse = uppercase(reverse(greet))
print(greet_upper_reverse("LabEx"))

これは以下のように出力されます。

!XEBAL,OLLEH

メソッドのデコレート

デコレータは、クラス内のメソッドの振る舞いを変更するためにも使用できます。同じ原則が適用されますが、デコレータ関数は最初の引数として self パラメータを受け取る必要があります。

def log_method(func):
    def wrapper(self, *args, **kwargs):
        print(f"Calling {func.__name__} on {self.__class__.__name__} with args={args} and kwargs={kwargs}")
        return func(self, *args, **kwargs)
    return wrapper

class Person:
    def __init__(self, name):
        self.name = name

    @log_method
    def greet(self, message):
        return f"{message}, {self.name}!"

person = Person("LabEx")
print(person.greet("Hello"))

これは以下のように出力されます。

Calling greet on Person with args=('Hello',) and kwargs={}
Hello, LabEx!

クラスのデコレート

デコレータは、クラス全体の振る舞いを変更するためにも使用できます。この場合、デコレータ関数はクラスを引数として受け取り、目的の振る舞いを持つ新しいクラスを返します。

def singleton(cls):
    instances = {}

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyClass:
    def __init__(self, value):
        self.value = value

obj1 = MyClass(42)
obj2 = MyClass(24)

print(obj1 is obj2)  ## True
print(obj1.value)    ## 42
print(obj2.value)    ## 42

この例では、singleton デコレータは、MyClass クラスが何度インスタンス化されようとも、そのクラスのインスタンスが 1 つだけ作成されることを保証します。

実践的なデコレータの例

ロギングデコレータ

デコレータの一般的な使用例は、関数呼び出しのロギングです。これはデバッグ、監視、または監査目的に役立ちます。

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_function_call
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

これは以下のように出力されます。

Calling add_numbers with args=(2, 3) and kwargs={}
5

キャッシングデコレータ

デコレータは、コストのかかる関数呼び出しの結果をキャッシュしてパフォーマンスを向上させるためにも使用できます。

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return (fibonacci(n-1) + fibonacci(n-2))

print(fibonacci(100))

functools モジュールの lru_cache デコレータは、関数の結果に対して最近最少使用 (Least Recently Used, LRU) キャッシュを実装する簡単な方法を提供します。

アクセス制御デコレータ

デコレータは、関数やメソッドへのアクセス制御を強制するために使用でき、特定の機能には承認されたユーザーのみがアクセスできるようにします。

from functools import wraps

def require_admin(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not is_admin(args[0]):
            raise ValueError("Access denied. You must be an admin.")
        return func(*args, **kwargs)
    return wrapper

class User:
    def __init__(self, name, is_admin):
        self.name = name
        self.is_admin = is_admin

    @require_admin
    def delete_user(self, user_to_delete):
        print(f"Deleting user: {user_to_delete.name}")

admin = User("LabEx", True)
regular_user = User("John", False)

admin.delete_user(regular_user)  ## Works
regular_user.delete_user(admin)  ## Raises ValueError

この例では、require_admin デコレータは、delete_user メソッドを呼び出すユーザーが管理者であるかどうかを確認してから操作を許可します。

リトライデコレータ

デコレータは、一時的な問題(ネットワークエラーや API のレート制限など)で失敗する可能性のある関数に対してリトライメカニズムを実装するためにも使用できます。

import time
from functools import wraps

def retry(max_retries=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Function {func.__name__} failed. Retrying... ({retries+1}/{max_retries})")
                    retries += 1
                    time.sleep(delay)
            raise Exception(f"Maximum number of retries ({max_retries}) reached for function {func.__name__}")
        return wrapper
    return decorator

@retry(max_retries=3, delay=2)
def flaky_function():
    ## Simulate a flaky function that fails 50% of the time
    if random.random() < 0.5:
        raise Exception("Oops, something went wrong!")
    return "Success!"

print(flaky_function())

この例では、retry デコレータは flaky_function を最大 3 回自動的にリトライし、各試行の間に 2 秒の遅延を挟んでから例外を発生させます。

これらは、Python のデコレータの実践的な使用例のほんの一部です。デコレータは強力で柔軟なツールであり、よりモジュール化され、保守可能で、再利用可能なコードを書くのに役立ちます。

まとめ

このチュートリアルの最後まで学ぶことで、単一の Python 関数に複数のデコレータを適用する方法をしっかりと理解することができます。実践的な例や実世界での使用例を学び、よりモジュール化され、柔軟で、保守可能な Python コードを書く力が身に付きます。