デコレータの連鎖とパラメータ付きデコレータ

PythonPythonBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

この実験では、Python のデコレータについて学びます。デコレータは、関数やメソッドの動作を変更できる強力な機能です。デコレータは、ロギング、パフォーマンス測定、アクセス制御、型チェックなどのタスクによく使用されます。

複数のデコレータを連鎖させる方法、パラメータを受け取るデコレータを作成する方法、デコレータを使用する際に関数のメタデータを保持する方法、およびさまざまなタイプのクラスメソッドにデコレータを適用する方法を学びます。作業するファイルは logcall.pyvalidate.py、および sample.py です。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/lambda_functions("Lambda Functions") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/class_static_methods("Class Methods and Static Methods") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/function_definition -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/lambda_functions -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/scope -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/classes_objects -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/class_static_methods -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} python/decorators -.-> lab-132515{{"デコレータの連鎖とパラメータ付きデコレータ"}} end

デコレータで関数のメタデータを保持する

Python では、デコレータは関数の動作を変更できる強力なツールです。ただし、デコレータを使って関数をラップすると、少し問題があります。デフォルトでは、元の関数のメタデータ(名前、ドキュメント文字列(docstring)、アノテーションなど)が失われます。メタデータは、イントロスペクション(コードの構造を調べること)やドキュメント生成に役立つため重要です。まずはこの問題を確認してみましょう。

WebIDE でターミナルを開きます。デコレータを使ったときに何が起こるかを確認するために、いくつかの Python コマンドを実行します。以下のコマンドで、デコレータでラップされた単純な関数 add を作成し、その関数とドキュメント文字列を出力します。

cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

これらのコマンドを実行すると、次のような出力が表示されます。

<function wrapper at 0x...>
None

関数名が add ではなく wrapper と表示されていることに注意してください。また、ドキュメント文字列は 'Adds two things' であるはずが、None となっています。これは、イントロスペクションツールやドキュメント生成ツールなど、このメタデータに依存するツールを使用する際に大きな問題となります。

functools.wraps で問題を解決する

Python の functools モジュールが助けになります。このモジュールには、関数のメタデータを保持するのに役立つ wraps デコレータが用意されています。logged デコレータを wraps を使うように修正する方法を見てみましょう。

  1. まず、WebIDE で logcall.py ファイルを開きます。ターミナルで次のコマンドを実行すると、プロジェクトディレクトリに移動できます。
cd ~/project
  1. 次に、logcall.pylogged デコレータを次のコードで更新します。ここで重要なのは @wraps(func) デコレータです。これにより、元の関数 func のすべてのメタデータがラッパー関数にコピーされます。
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
  1. @wraps(func) デコレータは重要な役割を果たします。元の関数 func のすべてのメタデータ(名前、ドキュメント文字列、アノテーションなど)を取得し、wrapper 関数に付加します。これにより、デコレートされた関数を使用するときに、正しいメタデータが保持されます。

  2. 改良したデコレータをテストしてみましょう。ターミナルで次のコマンドを実行します。

python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

すると、次のように表示されるはずです。

<function add at 0x...>
Adds two things

素晴らしい!関数名とドキュメント文字列が保持されています。これは、デコレータが期待通りに動作し、元の関数のメタデータが無傷であることを意味します。

validate.py のデコレータを修正する

次に、validate.pyvalidated デコレータにも同じ修正を適用しましょう。このデコレータは、関数のアノテーションに基づいて関数の引数と戻り値の型を検証するために使用されます。

  1. WebIDE で validate.py を開きます。

  2. validated デコレータを @wraps デコレータで更新します。次のコードはその方法を示しています。validated デコレータ内の wrapper 関数に @wraps(func) デコレータを追加することで、メタデータが保持されます。

from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper
  1. validated デコレータがメタデータを保持するようになったことをテストしましょう。ターミナルで次のコマンドを実行します。
python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
    'Multiplies two integers'
    return x * y
    
print(multiply)
print(multiply.__doc__)"

次のように表示されるはずです。

<function multiply at 0......>
Multiplies two integers

これで、loggedvalidated の両方のデコレータが、デコレートする関数のメタデータを適切に保持するようになりました。これにより、これらのデコレータを使用するときに、関数は依然として元の名前、ドキュメント文字列、アノテーションを持ち、コードの可読性と保守性に非常に役立ちます。

✨ 解答を確認して練習

引数を持つデコレータの作成

これまで、常に固定メッセージを出力する @logged デコレータを使用してきました。では、メッセージの形式をカスタマイズしたい場合はどうすればよいでしょうか?このセクションでは、引数を受け取る新しいデコレータを作成する方法を学び、デコレータの使用方法により柔軟性を持たせます。

パラメータ付きデコレータの理解

パラメータ付きデコレータは特殊な関数の一種です。他の関数を直接変更するのではなく、デコレータを返します。パラメータ付きデコレータの一般的な構造は次のようになります。

def decorator_with_args(arg1, arg2, ...):
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Use arg1, arg2, ... here
            ## Call the original function
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

コードで @decorator_with_args(value1, value2) を使用すると、Python はまず decorator_with_args(value1, value2) を呼び出します。この呼び出しは実際のデコレータを返し、それが @ 構文の後に続く関数に適用されます。この二段階のプロセスが、パラメータ付きデコレータの動作の鍵となります。

logformat デコレータの作成

フォーマット文字列(format string)を引数として受け取る @logformat(fmt) デコレータを作成しましょう。これにより、ログメッセージをカスタマイズできます。

  1. WebIDE で logcall.py を開き、新しいデコレータを追加します。以下のコードは、既存の logged デコレータと新しい logformat デコレータの定義方法を示しています。
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

logformat デコレータでは、外側の関数 logformat がフォーマット文字列 fmt を引数として受け取ります。そして、ターゲット関数を変更する実際のデコレータである decorator 関数を返します。

  1. 次に、sample.py を修正して新しいデコレータをテストしましょう。以下のコードは、異なる関数に loggedlogformat の両方のデコレータを使用する方法を示しています。
from logcall import logged, logformat

@logged
def add(x, y):
    "Adds two numbers"
    return x + y

@logged
def sub(x, y):
    "Subtracts y from x"
    return x - y

@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
    "Multiplies two numbers"
    return x * y

ここでは、add 関数と sub 関数は logged デコレータを使用し、mul 関数はカスタムフォーマット文字列で logformat デコレータを使用しています。

  1. 更新した sample.py を実行して結果を確認します。ターミナルを開き、次のコマンドを実行します。
cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"

次のような出力が表示されるはずです。

Calling add
5
sample.py:mul
6

この出力から、logged デコレータが期待通りに関数名を出力し、logformat デコレータがカスタムフォーマット文字列を使用してファイル名と関数名を出力していることがわかります。

logformat を使用して logged デコレータを再定義する

より柔軟な logformat デコレータができたので、これを使用して元の logged デコレータを再定義しましょう。これにより、コードの再利用が可能になり、一貫したログフォーマットを維持できます。

  1. logcall.py を次のコードで更新します。
from functools import wraps

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)

ここでは、ラムダ関数を使用して、logformat デコレータを基に logged デコレータを定義しています。ラムダ関数は関数 func を受け取り、特定のフォーマット文字列で logformat デコレータを適用します。

  1. 再定義した logged デコレータが引き続き機能することをテストします。ターミナルを開き、次のコマンドを実行します。
cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
    return f'Hello, {name}'
    
print(greet('World'))"

次のように表示されるはずです。

Calling greet
Hello, World

これは、再定義した logged デコレータが期待通りに動作し、logformat デコレータを成功裏に再利用して一貫したログフォーマットを実現したことを示しています。

✨ 解答を確認して練習

クラスメソッドにデコレータを適用する

ここでは、デコレータがクラスメソッドとどのように相互作用するかを探っていきます。Python にはインスタンスメソッド、クラスメソッド、静的メソッド、プロパティといった異なる種類のメソッドがあるため、これは少しトリッキーな場合があります。デコレータは、別の関数を受け取り、その関数の振る舞いを明示的に変更することなく拡張する関数です。クラスメソッドにデコレータを適用する際には、これらの異なるメソッドタイプとの相互作用に注意する必要があります。

チャレンジの理解

@logged デコレータを異なるタイプのメソッドに適用したときに何が起こるかを見てみましょう。@logged デコレータは、メソッド呼び出しに関する情報をログに記録するために使用される可能性があります。

  1. WebIDE で新しいファイル methods.py を作成します。このファイルには、@logged デコレータで装飾された異なるタイプのメソッドを持つクラスが含まれます。
from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @logged
    @classmethod
    def class_method(cls):
        print("Class method called")
        return "class result"

    @logged
    @staticmethod
    def static_method():
        print("Static method called")
        return "static result"

    @logged
    @property
    def property_method(self):
        print("Property method called")
        return "property result"

このコードでは、4 種類の異なるメソッドを持つ Spam クラスがあります。各メソッドは @logged デコレータで装飾されており、一部は @classmethod@staticmethod@property などの他の組み込みデコレータでも装飾されています。

  1. 動作をテストしてみましょう。ターミナルで Python コマンドを実行して、これらのメソッドを呼び出し、出力を確認します。
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

このコマンドを実行すると、いくつかの問題に気づくかもしれません。

  • @property デコレータは、@logged デコレータと正しく動作しない可能性があります。@property デコレータはメソッドをプロパティとして定義するために使用され、特定の動作方法があります。@logged デコレータと組み合わせると、競合が発生する可能性があります。
  • @classmethod@staticmethod の場合、デコレータの適用順序が重要です。デコレータの適用順序によって、メソッドの振る舞いが変わることがあります。

デコレータの適用順序

複数のデコレータを適用する場合、下から上に適用されます。つまり、メソッド定義に最も近いデコレータが最初に適用され、その後、上にあるデコレータが順番に適用されます。例えば:

@decorator1
@decorator2
def func():
    pass

これは次と等価です。

func = decorator1(decorator2(func))

この例では、decorator2func に最初に適用され、次に decorator1decorator2(func) の結果に適用されます。

デコレータの順序を修正する

methods.py ファイルを更新して、デコレータの順序を修正しましょう。デコレータの順序を変更することで、各メソッドが期待通りに動作するようにできます。

from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @classmethod
    @logged
    def class_method(cls):
        print("Class method called")
        return "class result"

    @staticmethod
    @logged
    def static_method():
        print("Static method called")
        return "static result"

    @property
    @logged
    def property_method(self):
        print("Property method called")
        return "property result"

この更新版では:

  • instance_method の場合、順序は関係ありません。インスタンスメソッドはクラスのインスタンスで呼び出され、@logged デコレータは基本的な機能に影響を与えることなく任意の順序で適用できます。
  • class_method の場合、@logged の後に @classmethod を適用します。@classmethod デコレータはメソッドの呼び出し方法を変更し、@logged の後に適用することで、ログ記録が正しく機能することが保証されます。
  • static_method の場合、@logged の後に @staticmethod を適用します。@classmethod と同様に、@staticmethod デコレータには独自の振る舞いがあり、@logged デコレータとの順序が正しくなければなりません。
  • property_method の場合、@logged の後に @property を適用します。これにより、プロパティの振る舞いが維持されると同時に、ログ記録機能も得られます。
  1. 更新したコードをテストしてみましょう。前と同じコマンドを実行して、問題が解決したかどうかを確認します。
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

これで、すべてのメソッドタイプに対して適切なログ記録が行われるはずです。

Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result

メソッドデコレータのベストプラクティス

メソッドデコレータを使用する際には、次のベストプラクティスに従ってください。

  1. メソッドを変換するデコレータ(@classmethod@staticmethod@property)は、カスタムデコレータのに適用します。これにより、カスタムデコレータがログ記録やその他の操作を最初に実行でき、その後、組み込みデコレータがメソッドを意図した通りに変換できます。
  2. デコレータの実行は、メソッド呼び出し時ではなく、クラス定義時に行われることに注意してください。これは、デコレータ内のセットアップまたは初期化コードが、メソッドが呼び出されるのではなく、クラスが定義されたときに実行されることを意味します。
  3. より複雑なケースでは、異なるメソッドタイプに対して専用のデコレータを作成する必要があるかもしれません。異なるメソッドタイプには異なる振る舞いがあり、ワンサイズフィットオールのデコレータはすべての状況で機能するとは限りません。
✨ 解答を確認して練習

引数を持つ型強制デコレータの作成

前のステップでは、@validated デコレータについて学びました。このデコレータは、Python 関数で型アノテーションを強制するために使用されます。型アノテーションは、関数の引数と戻り値の期待される型を指定する方法です。今回は、さらに一歩進んで、型指定を引数として受け取ることができる、より柔軟なデコレータを作成します。これにより、各引数と戻り値に対して、より明示的に型を定義することができます。

目標の理解

私たちの目標は、@enforce() デコレータを作成することです。このデコレータを使用すると、キーワード引数を使って型制約を指定することができます。以下は、その動作の例です。

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

この例では、@enforce デコレータを使用して、add 関数の xy 引数が Integer 型であるべきこと、および戻り値も Integer 型であるべきことを指定しています。このデコレータは、前の @validated デコレータと同様の動作をしますが、型指定に対するコントロールがより強化されています。

enforce デコレータの作成

  1. まず、WebIDE で validate.py ファイルを開きます。このファイルに新しいデコレータを追加します。追加するコードは次のとおりです。
from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper

def enforce(**type_specs):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Check argument types
            for arg_name, arg_value in zip(func.__code__.co_varnames, args):
                if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
                    raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')

            ## Run the function and get the result
            result = func(*args, **kwargs)

            ## Check the return value
            if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
                raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')

            return result
        return wrapper
    return decorator

このコードが何をするかを分解してみましょう。Integer クラスは、カスタム型を定義するために使用されます。validated デコレータは、関数の型アノテーションに基づいて、関数の引数と戻り値の型をチェックします。enforce デコレータは、私たちが作成する新しいデコレータです。これは、各引数と戻り値の型を指定するキーワード引数を受け取ります。enforce デコレータの wrapper 関数内では、引数と戻り値の型が指定された型と一致するかどうかをチェックします。一致しない場合は、TypeError を発生させます。

  1. 次に、新しい @enforce デコレータをテストしましょう。いくつかのテストケースを実行して、期待通りに動作するかどうかを確認します。テストを実行するコードは次のとおりです。
cd ~/project
python3 -c "from validate import enforce, Integer

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

## This should work
print(add(2, 3))

## This should raise a TypeError
try:
    print(add('2', 3))
except TypeError as e:
    print(f'Error: {e}')

## This should raise a TypeError
try:
    @enforce(x=Integer, y=Integer, return_=Integer)
    def bad_add(x, y):
        return str(x + y)
    print(bad_add(2, 3))
except TypeError as e:
    print(f'Error: {e}')"

このテストコードでは、まず @enforce デコレータを使用して add 関数を定義します。次に、有効な引数で add 関数を呼び出します。これはエラーなく動作するはずです。次に、無効な引数で add 関数を呼び出します。これは TypeError を発生させるはずです。最後に、誤った型の値を返す bad_add 関数を定義します。これも TypeError を発生させるはずです。

このテストコードを実行すると、次のような出力が表示されるはずです。

5
Error: Expected x to be Integer
Error: Expected return value to be Integer

この出力は、@enforce デコレータが正しく動作していることを示しています。引数または戻り値の型が指定された型と一致しない場合、TypeError を発生させます。

2 つのアプローチの比較

@validated デコレータと @enforce デコレータはどちらも、型制約を強制するという同じ目標を達成しますが、方法が異なります。

  1. @validated デコレータは、Python の組み込み型アノテーションを使用します。以下は例です。

    @validated
    def add(x: Integer, y: Integer) -> Integer:
        return x + y

    このアプローチでは、型アノテーションを使用して、関数定義内で直接型を指定します。これは Python の組み込み機能であり、統合開発環境(IDE)でより良いサポートが提供されます。IDE はこれらの型アノテーションを使用して、コード補完、型チェック、その他の便利な機能を提供することができます。

  2. 一方、@enforce デコレータは、キーワード引数を使用して型を指定します。以下は例です。

    @enforce(x=Integer, y=Integer, return_=Integer)
    def add(x, y):
        return x + y

    このアプローチは、型指定を直接デコレータの引数として渡しているため、より明示的です。他のアノテーションシステムに依存するライブラリを使用する場合に便利です。

各アプローチにはそれぞれ利点があります。型アノテーションは Python のネイティブな機能であり、IDE のサポートが良好です。一方、@enforce アプローチは、より柔軟性と明示性を提供します。作業しているプロジェクトに応じて、最適なアプローチを選択することができます。

✨ 解答を確認して練習

まとめ

この実験では、デコレータを効果的に作成して使用する方法を学びました。functools.wraps を使って関数のメタデータを保持する方法、引数を受け取るデコレータを作成する方法、複数のデコレータを扱いその適用順序を理解する方法を学びました。また、異なるクラスメソッドにデコレータを適用する方法や、引数を取る型強制デコレータを作成する方法も学びました。

これらのデコレータパターンは、Flask、Django、pytest などの Python フレームワークで一般的に使用されています。デコレータを習得することで、より保守可能で再利用可能なコードを書くことができるようになります。さらなる学習のために、コンテキストマネージャー、クラスベースのデコレータ、デコレータを使ったキャッシュ、デコレータによる高度な型チェックなどを探索することができます。