Python デコレータ

PythonBeginner

はじめに

この実験(Lab)では、Python のデコレータについて学びます。デコレータは、関数やクラスの機能を永続的に変更することなく、簡潔かつ再利用可能な方法で拡張するための強力な機能です。

まず、シンプルなデコレータの構築から始め、次に、関数の引数の扱い方、functools.wrapsを使用したベストプラクティスの適用、独自の引数を受け入れるデコレータの作成方法、そして最後に、状態を管理するためのクラスを使用したデコレータの実装方法を学習します。

最小限のデコレータ

このステップでは、Python デコレータの基本的な構造について学習します。デコレータは、最も単純な形では、別の関数を引数として受け取り、何らかの機能を追加してから、別の関数を返す関数です。

注記: WebIDE(VS Code のようなもの)の使用に慣れていない場合は、まずこちらの実験 (lab)で学習してください。

まず、左側のファイルエクスプローラーからファイル step1.py を開きます。

次に、以下のコードを step1.py ファイルにコピーします。このコードは、単純なデコレータを定義し、それを関数に適用します。

## デコレータ:別の関数を受け取り、ラッパーを返す関数
def your_decorator(func):
  def wrapper():
    ## func の前に処理を行う...
    print("Before func!")
    func()  ## 元の関数を呼び出す
    ## func の後に処理を行う...
    print("After func!")
  return wrapper  ## ラッパー関数を返す

## @your_decorator は、foo = your_decorator(foo) のシンタックスシュガーです
@your_decorator
def foo():
  print("Hello World!")

foo()  ## ラッパーが呼び出され、追加の動作を伴って foo が呼び出されます

コードを詳しく見てみましょう:

  • your_decorator(func): これはデコレータ関数です。引数として、デコレートされる関数 func を一つ取ります。
  • wrapper(): これは元の関数を「ラップする」内部関数です。追加したい新しい動作を含みます。ここでは、元の関数を呼び出す前後にメッセージを出力します。
  • @your_decorator: これはデコレータを適用するためのシンタックスシュガーです。関数定義の後に foo = your_decorator(foo) と書くことと同等です。

それでは、ターミナルでスクリプトを実行して、デコレータがどのように機能するかを確認しましょう。

python3 step1.py

以下の出力が表示されるはずです。これは、wrapper 関数が実行され、元の foo 関数の周りに動作が追加されたことを示しています。

Before func!
Hello World!
After func!

引数を持つ関数のためのデコレータ

このステップでは、引数を受け取る関数でも動作するようにデコレータを拡張します。実世界の関数は引数を持つことが多いため、デコレータもそれらを正しく処理できる必要があります。

これを行うには、ラッパー関数のシグネチャで *args と **kwargs を使用できます。これにより、ラッパーは任意の数の位置引数およびキーワード引数を受け取り、それらを元の関数に渡すことができるようになります。

ファイル step2.py を開き、以下のコードを追加してください。

## Decorator that works with functions that have parameters
def your_decorator(func):
  def wrapper(*args,**kwargs):  ## Accept any arguments
    ## Do stuff before func...
    print("Before func!")
    func(*args,**kwargs)  ## Pass arguments to original function
    ## Do stuff after func...
    print("After func!")
  return wrapper

@your_decorator
def foo(bar):
  print("My name is " + bar)

foo("Jack")  ## Arguments are passed through wrapper

このコードでは、wrapper 関数が *args および **kwargs を使用して定義されています。foo("Jack") が呼び出されると、引数 "Jack" は wrapper 内の *args によってキャプチャされ、func(*args, **kwargs) が実行されるときに元の func に渡されます。

スクリプトを実行して結果を確認してください。

python3 step2.py

出力は、引数がデコレータを介して元の関数に正常に渡されたことを示しています。

Before func!
My name is Jack
After func!

基本的なデコレータのテンプレート

このステップでは、堅牢なデコレータを作成するためのベストプラクティスのテンプレートについて学習します。これまでのデコレータは機能しましたが、副作用がありました。それは、元の関数のメタデータ(名前 __name__ や docstring __doc__ など)を失ってしまうことです。

これを解決するために、Python の functools モジュールは wraps というヘルパーデコレータを提供しています。さらに、良いデコレータは戻り値を返す関数を処理できる必要があります。

このテンプレートは、ほとんどのデコレータの使用例に役立ちます。引数の有無、戻り値の有無にかかわらず有効です。

ファイル step3.py を開き、以下のコードを追加してください。

import functools

## Best practice decorator template: preserves function metadata and return value
def your_decorator(func):
  @functools.wraps(func)  ## Preserves function name, docstring, etc.
  def wrapper(*args,**kwargs):
    print("Before function execution...")
    result = func(*args,**kwargs)  ## Call function and capture return value
    print("After function execution...")
    return result  ## Return the original function's return value
  return wrapper

@your_decorator
def add(a, b):
  """This function adds two numbers."""
  return a + b

sum_result = add(5, 3)
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")
print(f"Result: {sum_result}")

このテンプレートにおける主な改善点:

  • import functools: 必要なモジュールをインポートします。
  • @functools.wraps(func): これは wrapper 関数に適用されるデコレータです。元の func からのメタデータ(__name__ や __doc__ など)を wrapper 関数にコピーします。
  • result = func(*args, **kwargs): 元の関数の戻り値が result 変数にキャプチャされます。
  • return result: ラッパー関数はキャプチャされた結果を返すため、装飾された関数は期待どおりに動作します。

スクリプトを実行します。

python3 step3.py

出力は、関数の名前と docstring が保持され、戻り値が正しく処理されていることを示しています。

Before function execution...
After function execution...
Function name: add
Docstring: This function adds two numbers.
Result: 8

引数を持つデコレータ

このステップでは、独自の引数でカスタマイズできるデコレータを作成する方法を学習します。これにより、@your_decorator(arg='value') のような、より柔軟で設定可能なデコレータの使用が可能になります。

これを実現するには、「デコレータファクトリ」—引数を受け取りデコレータを返す関数—を作成する必要があります。これにより、構造にネストの層が追加されます。

prefix 文字列を受け取り、関数呼び出しの前後にそれを出力するデコレータを作成してみましょう。step4.py を開き、以下のコードを追加してください。

import functools

## Decorator factory: returns a decorator based on parameters
def prefix_decorator(prefix):
  def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
      ## Use the 'prefix' argument from the factory
      print(f"{prefix} Before function call")
      result = func(*args, **kwargs)
      print(f"{prefix} After function call")
      return result
    return wrapper
  return decorator  ## Return the actual decorator function

## Using the decorator with a parameter
@prefix_decorator(prefix="LOG:")
def say_hello(name):
  print(f"Hello, {name}!")

say_hello("Alice")

動作の仕組みは次のとおりです。

  1. 最初に prefix_decorator(prefix="LOG:") が呼び出されます。これがファクトリ関数です。
  2. これは decorator 関数を返し、この decorator 関数はクロージャ(closure)を介して prefix 変数("LOG:")にアクセスできるようになります。
  3. 返されたこの decorator が say_hello に適用され、装飾されます。

引数がどのように使用されているかを確認するために、スクリプトを実行します。

python3 step4.py

出力は、デコレータに引数として渡された "LOG:" プレフィックスが表示されていることを示しています。

LOG: Before function call
Hello, Alice!
LOG: After function call

クラスベースのデコレータ

このステップでは、クラスを使用してデコレータを実装する方法を探ります。このアプローチは、関数呼び出し間で状態を維持する場合に特に役立ちます。クラス内のメソッドを装飾するパターンと、デコレータ自体をクラスとして定義するパターンの 2 つを取り上げます。

始めるために、ファイル step5.py を開いてください。

パターン 1: クラスメソッドの装飾

クラス内にデコレータをメソッドとして定義し、同じクラスの他のメソッドに適用することができます。これは、クラスのコンテキストに結びついた特定の動作を作成する場合に役立ちます。

step5.py に以下のコードを追加します。この例では、クラスメソッドの例外を処理するためのデコレータを定義します。

## Pattern 1: Decorating a method within a class
class DecorateMyMethod:

  ## Static method decorator for methods with only 'self' parameter
  def decorator_for_class_method_with_no_args(method):
    def wrapper_for_class_method(self):  ## Only takes self
      try:
        return method(self)  ## Call original method
      except Exception as e:
        print(f"\nWARNING: An exception occurred: {e}\n")
    return wrapper_for_class_method

  def __init__(self,succeed:bool):
    self.succeed = succeed

  @decorator_for_class_method_with_no_args
  def class_action(self):
    if self.succeed:
      print("You succeeded by choice.")
    else:
      raise Exception("Epic fail of your own creation.")

print("--- Testing Pattern 1 ---")
test_succeed = DecorateMyMethod(True)
test_succeed.class_action()

test_fail = DecorateMyMethod(False)
test_fail.class_action()
print("-" * 25)

パターン 2: クラスとしてのデコレータ

より一般的で強力なパターンは、デコレータ自体をクラスとして定義することです。これは、デコレータが状態を維持する必要がある場合に最適です。__init__() メソッドと __call__() メソッドを実装することで、クラスのインスタンスを関数のように振る舞わせることができます。

関数が呼び出された回数をカウントするデコレータを作成してみましょう。step5.py の末尾に以下のコードを追加します。

## Pattern 2: Class-based decorator for maintaining state
class CountCallNumber:

  def __init__(self, func):
    self.func = func  ## Store the function to decorate
    self.call_number = 0  ## Initialize call counter

  def __call__(self, *args, **kwargs):  ## Makes instance callable
    self.call_number += 1  ## Increment counter
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)  ## Call original function

@CountCallNumber
def say_hi(name):
  print("Hi! My name is " + name)

print("\n--- Testing Pattern 2 ---")
say_hi("Jack")
say_hi("James")
  • __init__(self, func): これはコンストラクタです。デコレータが適用されるとき(@CountCallNumber)に呼び出されます。装飾される関数(say_hi)を受け取り、それを保存します。
  • __call__(self, *args, **kwargs): このメソッドはクラスインスタンスを呼び出し可能にします。装飾された関数(say_hi("Jack"))を呼び出すとき、実際にはこの __call__ メソッドが呼び出されます。これはカウンターをインクリメントし、その後、保存されている元の関数を呼び出します。

それでは、スクリプト全体を実行します。

python3 step5.py

両方のパターンの出力が表示されます。2 番目の部分で呼び出しカウンターが増加していることに注目してください。

--- Testing Pattern 1 ---
You succeeded by choice.

WARNING: An exception occurred: Epic fail of your own creation.

-------------------------

--- Testing Pattern 2 ---
This is execution number 1
Hi! My name is Jack
This is execution number 2
Hi! My name is James

まとめ

この実験(Lab)の完了、おめでとうございます!Python のデコレータとその様々な形式について、しっかりとした理解が得られたはずです。

この実験(Lab)では、以下のことを学びました。

  • 基本的なデコレータを作成し、@ 構文を理解する方法。
  • *args および **kwargs を使用して、任意の数の引数を受け入れる関数で動作するデコレータを設計する方法。
  • 関数メタデータを保持し、戻り値を処理するために functools.wraps を使用したベストプラクティスのテンプレート。
  • デコレータをパラメータでカスタマイズできるようにするデコレータファクトリの作成方法。
  • 関数呼び出し間で状態(例:呼び出し回数カウンター)を維持するために、デコレータをクラスとして実装する方法。

デコレータは、クリーンで再利用可能で拡張性の高い Python コードを書くための重要な機能です。これらのパターンを適用して、ご自身のプロジェクトを強化することができます。