ホームページ  >  記事  >  バックエンド開発  >  Python のデコレータとは何ですか?デコレーターはどのように働くのですか?

Python のデコレータとは何ですか?デコレーターはどのように働くのですか?

coldplay.xixi
coldplay.xixi転載
2021-03-08 09:49:262728ブラウズ

Python のデコレータとは何ですか?デコレーターはどのように働くのですか?

Python は、最初の定義後に定義する必要がある関数とメソッドの定義方法を簡素化するメカニズムとして、PEP-318 で非常に早い段階でデコレーターを導入しました。

これを行う当初の動機の 1 つは、classmethod や staticmethod などの関数を使用してメソッドの元の定義を変換することでしたが、関数の初期定義を変更するには追加のコード行が必要になります。

一般的に、変換を関数に適用する必要がある場合は常に、修飾子関数を使用して関数を呼び出し、関数が最初に定義されたときの名前に再割り当てする必要があります。

たとえば、original という関数があり、その関数に元の動作を変更する関数 (modifier と呼ばれる) があるとします。その場合、次のように記述する必要があります:

def original(...):
    ...
original = modifier(original)

(関連無料学習 推奨: Python ビデオ チュートリアル )

関数をどのように変更し、同じ名前に再割り当てしたかに注意してください。これは混乱を招き、エラーが発生しやすくなります (誰かが関数の再割り当てを忘れたり、関数定義の後の行ではなく、さらに先のどこかで関数を再割り当てしたりすると仮定します)。このため、Python 言語にはいくつかの構文サポートが追加されました。

前の例は次のように書き換えることができます:

@modifier
def original(...):
   ...

これは、デコレータ自体の最初のパラメータとしてデコレータを呼び出した後の、デコレータはコンテンツの単なる構文糖であることを意味します。デコレータが返すものになります。

Python の用語と一致させるために、この例では modifier はデコレーターと呼ばれ、original は装飾関数であり、多くの場合ラッパー オブジェクトとも呼ばれます。

この機能はもともとメソッドと関数のためのものと考えられていましたが、実際の構文ではあらゆる種類のオブジェクトを装飾できるため、関数、メソッド、ジェネレーター、およびクラスに適用されるデコレーターを見ていきます。

最後に注意すべき点は、デコレーターの名前は正しいのですが (結局のところ、デコレーターはラップされた関数を実際に変更、拡張、または処理しているのです)、それをデコレーターのデザイン パターンと比較しないでください。 。

5.1.1 デコレータ関数

関数はおそらく、装飾できる Python オブジェクトの最も単純な表現です。関数にデコレーターを使用して、あらゆる種類のロジックを適用できます。パラメーターの検証、前提条件のチェック、動作の完全な変更、署名の変更、結果のキャッシュ (元の関数のメモリ内バージョンの作成) などが可能です。

たとえば、再試行メカニズムを実装し、特定のドメイン レベルの例外を制御し、特定の回数再試行する基本的なデコレータを作成します。

# decorator_function_1.py
class ControlledException(Exception):
    """A generic exception on the program's domain."""

def retry(operation):
    @wraps(operation)
    def wrapped(*args, **kwargs):
        last_raised = None
        RETRIES_LIMIT = 3
        for _ in range(RETRIES_LIMIT):
            try:
                return operation(*args, **kwargs)
            except ControlledException as e:
                logger.info("retrying %s", operation.__qualname__)
                last_raised = e
        raise last_raised

    return wrapped

@wrap の使用方法は次のとおりです。別のセクションで説明するため、今は無視します。 for ループで「_」を使用すると、for ループでは使用されていないため、現時点では関心のない変数に番号が割り当てられることを意味します (Python では、無視された値に名前を付けるのが一般的です) _" 熟語)。

再試行デコレータはパラメータを受け取らないため、次のように任意の関数に簡単に適用できます:

@retry
def run_operation(task):
    """Run a particular task, simulating some failures on its execution."""
    return task.run()

冒頭で説明したように、run_operation @retry の上に定義します。 run_operation = retry(run_operation) を実際に実行するために Python によって提供される糖衣構文。

この限定的な例では、デコレーターを使用して、特定の条件下 (この例ではタイムアウト関連の例外として表現されています) で汎用的な再試行操作を作成する方法を示します。装飾されたコードを複数回呼び出す必要があります。

5.1.2 装飾クラス

クラスも装飾することができ (PEP-3129)、その装飾方法は構文関数の方法と同じです。唯一の違いは、デコレーターのコードを記述するときに、受信したものが関数ではなくクラスであることを考慮する必要があることです。

一部の実践者は、クラスの装飾は非常に複雑な問題であると考えるかもしれません。そのようなシナリオでは、クラス内でいくつかのプロパティとメソッドを宣言することになるため、読みやすさが損なわれる可能性がありますが、舞台裏ではデコレーターがいくつかの変更を適用する可能性があります。したがって、まったく異なるクラスがレンダリングされます。

この評価は正しいですが、装飾技術が深刻に乱用された場合にのみ当てはまります。客観的には、これは関数を装飾することと何ら変わりません。結局のところ、クラスは関数と同様、Python エコシステムにおけるオブジェクトの 1 つのタイプにすぎません。この問題の長所と短所についてはセクション 5.4 で再度説明しますが、ここではデコレータ、特にクラスに適用されるデコレータの長所についてのみ説明します。

(1) コードの再利用と DRY 原則のすべての利点。クラス デコレータの便利な例は、(複数のクラスに適用されるデコレータで 1 つのチェックのみを行うことによって) 複数のクラスを特定のインターフェイスまたは標準に強制的に準拠させることです。

(2) より小さいクラスまたはより単純なクラスを作成できます。これらのクラスは、後でデコレータによって拡張されます。

(3) デコレータを使用すると、メタクラスなどのより複雑な (通常は推奨されない) メソッドを使用せずに、特定のクラスに適用する必要がある変換ロジックの保守が容易になります。

在装饰器的所有可能应用程序中,我们将探索一个简单的示例,以了解装饰器可以用于哪些方面。记住,这不是类装饰器的唯一应用程序类型,而且给出的代码还可以有许多其他解决方案。所有这些解决方案都有优缺点,之所以选择装饰器,是为了说明它们的用处。

回顾用于监视平台的事件系统,现在需要转换每个事件的数据并将其发送到外部系统。然而,在选择如何发送数据时,每种类型的事件可能都有自己的特殊性。

特别是,登录事件可能包含敏感信息,例如我们希望隐藏的凭据。时间戳等其他领域的字段可能也需要一些转换,因为我们希望以特定的格式显示它们。符合这些要求的第一次尝试很简单,就像有一个映射到每个特定事件的类,并知道如何序列化它那样:

class LoginEventSerializer:
    def __init__(self, event):
        self.event = event

    def serialize(self) -> dict:
        return {
            "username": self.event.username,
            "password": "**redacted**",
            "ip": self.event.ip,
            "timestamp": self.event.timestamp.strftime("%Y-%m-%d
             %H:%M"),
        }

class LoginEvent:
    SERIALIZER = LoginEventSerializer

    def __init__(self, username, password, ip, timestamp):
        self.username = username
        self.password = password
        self.ip = ip
        self.timestamp = timestamp

    def serialize(self) -> dict:
        return self.SERIALIZER(self).serialize()

在这里,我们声明一个类。该类将直接映射到登录事件,其中包含它的一些逻辑——隐藏密码字段,并根据需要格式化时间戳。

虽然这是可行的,可能开始看起来是一个不错的选择,但随着时间的推移,若要扩展系统,就会发现一些问题。

(1)类太多。随着事件数量的增多,序列化类的数量将以相同的量级增长,因为它们是一一映射的。

(2)解决方案不够灵活。如果我们需要重用部分组件(例如,需要把密码藏在也有类似需求的另一个类型的事件中),就不得不将其提取到一个函数,但也要从多个类中调用它,这意味着我们没有重用那么多代码。

(3)样板文件。serialize()方法必须出现在所有事件类中,同时调用相同的代码。尽管我们可以将其提取到另一个类中(创建mixin),但这似乎没有很好地使用继承。

另一种解决方案是能够动态构造一个对象:给定一组过滤器(转换函数)和一个事件实例,该对象能够通过将过滤器应用于其字段的方式序列化它。然后,我们只需要定义转换每种字段类型的函数,并通过组合这些函数创建序列化器。

一旦有了这个对象,我们就可以装饰类以添加serialize()方法。该方法只会调用这些序列化对象本身:

def hide_field(field) -> str:
    return "**redacted**"

def format_time(field_timestamp: datetime) -> str:
    return field_timestamp.strftime("%Y-%m-%d %H:%M")

def show_original(event_field):
    return event_field

class EventSerializer:
    def __init__(self, serialization_fields: dict) -> None:
        self.serialization_fields = serialization_fields

    def serialize(self, event) -> dict:
        return {
            field: transformation(getattr(event, field))
            for field, transformation in
            self.serialization_fields.items()
        }

class Serialization:
    def __init__(self, **transformations):
        self.serializer = EventSerializer(transformations)

    def __call__(self, event_class):
        def serialize_method(event_instance):
                return self.serializer.serialize(event_instance)
            event_class.serialize = serialize_method
            return event_class

@Serialization(
    username=show_original,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
class LoginEvent:

    def __init__(self, username, password, ip, timestamp):
        self.username = username
        self.password = password
        self.ip = ip
        self.timestamp = timestamp

注意,装饰器让你更容易知道如何处理每个字段,而不必查看另一个类的代码。仅通过读取传递给类装饰器的参数,我们就知道用户名和IP地址将保持不变,密码将被隐藏,时间戳将被格式化。

现在,类的代码不需要定义serialize()方法,也不需要从实现它的mixin类进行扩展,因为这些都将由装饰器添加。实际上,这可能是创建类装饰器的唯一理由,因为如果不是这样的话,序列化对象可能是LoginEvent的一个类属性,但是它通过向该类添加一个新方法来更改类,这使得创建该类装饰器变得不可能。

我们还可以使用另一个类装饰器,通过定义类的属性来实现init方法的逻辑,但这超出了本例的范围。

通过使用Python 3.7+ 中的这个类装饰器(PEP-557),可以按更简洁的方式重写前面的示例,而不使用init的样板代码,如下所示:

from dataclasses import dataclass
from datetime import datetime

@Serialization(
    username=show_original,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
@dataclass
class LoginEvent:
    username: str
    password: str
    ip: str
    timestamp: datetime

5.1.3 其他类型的装饰器

既然我们已经知道了装饰器的@语法的实际含义,就可以得出这样的结论:可以装饰的不仅是函数、方法或类;实际上,任何可以定义的东西(如生成器、协同程序甚至是装饰过的对象)都可以装饰,这意味着装饰器可以堆叠起来。

前面的示例展示了如何链接装饰器。我们先定义类,然后将@dataclass应用于该类——它将该类转换为数据类,充当这些属性的容器。之后,通过@Serialization把逻辑应用到该类上,从而生成一个新类,其中添加了新的serialize()方法。

装饰器另一个好的用法是用于应该用作协同程序的生成器。我们将在第7章中探讨生成器和协同程序的细节,其主要思想是,在向新创建的生成器发送任何数据之前,必须通过调用next()将后者推进到下一个yield语句。这是每个用户都必须记住的手动过程,因此很容易出错。我们可以轻松创建一个装饰器,使其接收生成器作为参数,调用next(),然后返回生成器。

5.1.4 将参数传递给装饰器

至此,我们已经将装饰器看作Python中的一个强大工具。如果我们可以将参数传递给装饰器,使其逻辑更加抽象,那么其功能可能会更加强大。

有几种实现装饰器的方法可以接收参数,但是接下来我们只讨论最常见的方法。第一种方法是将装饰器创建为带有新的间接层的嵌套函数,使装饰器中的所有内容深入一层。第二种方法是为装饰器使用一个类。

通常,第二种方法更倾向于可读性,因为从对象的角度考虑,其要比3个或3个以上使用闭包的嵌套函数更容易。但是,为了完整起见,我们将对这两种方法进行探讨,以便你可以选择使用最适合当前问题的方法。

1.带有嵌套函数的装饰器

粗略地说,装饰器的基本思想是创建一个返回函数的函数(通常称为高阶函数)。在装饰器主体中定义的内部函数将是实际被调用的函数。

现在,如果希望将参数传递给它,就需要另一间接层。第一个函数将接收参数,在该函数中,我们将定义一个新函数(它将是装饰器),而这个新函数又将定义另一个新函数,即装饰过程返回的函数。这意味着我们将至少有3层嵌套函数。

如果你到目前为止还不明白上述内容的含义,也不用担心,待查看下面给出的示例之后,就会明白了。

第一个示例是,装饰器在一些函数上实现重试功能。这是个好主意,只是有个问题:实现不允许指定重试次数,只允许在装饰器中指定一个固定的次数。

现在,我们希望能够指出每个示例有多少次重试,也许甚至可以为这个参数添加一个默认值。为了实现这个功能,我们需要用到另一层嵌套函数——先用于参数,然后用于装饰器本身。

这是因为如下代码:

@retry(arg1, arg2,... )

必须返回装饰器,因为@语法将把计算结果应用到要装饰的对象上。从语义上讲,它可以翻译成如下内容:

<original_function> = retry(arg1, arg2, ....)(<original_function>)

除了所需的重试次数,我们还可以指明希望控制的异常类型。支持新需求的新版本代码可能是这样的:

RETRIES_LIMIT = 3

def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None):
    allowed_exceptions = allowed_exceptions or (ControlledException,)

    def retry(operation):

        @wraps(operation)
        def wrapped(*args, **kwargs):
            last_raised = None
            for _ in range(retries_limit):
                try:
                    return operation(*args, **kwargs)
                except allowed_exceptions as e:
                    logger.info("retrying %s due to %s", operation, e)
                    last_raised = e
            raise last_raised

        return wrapped

    return retry

下面是这个装饰器如何应用于函数的一些示例,其中显示了它接收的不同选项:

# decorator_parametrized_1.py
@with_retry()
def run_operation(task):
    return task.run()

@with_retry(retries_limit=5)
def run_with_custom_retries_limit(task):
    return task.run()

@with_retry(allowed_exceptions=(AttributeError,))
def run_with_custom_exceptions(task):
    return task.run()

@with_retry(
    retries_limit=4, allowed_exceptions=(ZeropisionError, AttributeError)
)
def run_with_custom_parameters(task):
    return task.run()

2.装饰器对象

前面的示例需要用到3层嵌套函数。首先,这将是一个用于接收我们想要使用的装饰器的参数。在这个函数中,其余的函数是使用这些参数和装饰器逻辑的闭包。

更简洁的实现方法是用一个类定义装饰器。在这种情况下,我们可以在__init__方法中传递参数,然后在名为__call__的魔法方法上实现装饰器的逻辑。

装饰器的代码如下所示:

class WithRetry:

    def __init__(self, retries_limit=RETRIES_LIMIT,
allowed_exceptions=None):
        self.retries_limit = retries_limit
        self.allowed_exceptions = allowed_exceptions or
(ControlledException,)

    def __call__(self, operation):

        @wraps(operation)
        def wrapped(*args, **kwargs):
            last_raised = None

            for _ in range(self.retries_limit):
                try:
                    return operation(*args, **kwargs)
                except self.allowed_exceptions as e:
                    logger.info("retrying %s due to %s", operation, e)
                    last_raised = e
            raise last_raised

        return wrapped

这个装饰器可以像之前的一样应用,就像这样:

@WithRetry(retries_limit=5)
def run_with_custom_retries_limit(task):
    return task.run()

注意Python语法在这里是如何起作用的,这一点很重要。首先,我们创建对象,这样在应用@操作之前,对象已经创建好了,并且其参数传递给它了,用这些参数初始化这个对象,如init方法中定义的那样。在此之后,我们将调用@操作,这样该对象将包装名为run_with_custom_reries_limit的函数,而这意味着它将被传递给call这个魔法方法。

在call这个魔法方法中,我们定义了装饰器的逻辑,就像通常所做的那样——包装了原始函数,返回一个新的函数,其中包含所要的逻辑。

5.1.5 充分利用装饰器

本节介绍一些充分利用装饰器的常见模式。在有些常见的场景中使用装饰器是个非常好的选择。

可用于应用程序的装饰器数不胜数,下面仅列举几个最常见或相关的。

(1)转换参数。更改函数的签名以公开更好的API,同时封装关于如何处理和转换参数的详细信息。

(2)跟踪代码。记录函数及其参数的执行情况。

(3)验证参数

(4)实现重试操作

(5)通过把一些(重复的)逻辑移到装饰器中来简化类

接下来详细讨论前两个应用程序。

1.转换参数

前文提到,装饰器可以用来验证参数(甚至在DbC的概念下强制一些前置条件或后置条件),因此你可能已经了解到,这是一些处理或者操控参数时使用装饰器的常用方法。

特别是,在某些情况下,我们会发现自己反复创建类似的对象,或者应用类似的转换,而我们希望将这些转换抽象掉。大多数时候,我们可以通过简单地用装饰器实现这一点。

2.跟踪代码

在本节中讨论跟踪时,我们将提到一些更通用的内容,这些内容与处理所要监控的函数的执行有关,具体是指:

(1) 関数の実行を実際に追跡します (たとえば、関数の実行行を記録することによって);

(2) 関数のいくつかの指標を監視します (CPU 使用率やメモリ使用率など)。 ;

(3) 関数の実行時間を測定します;

(4) 関数が呼び出されたとき、および関数に渡されたパラメータをログに記録します。

セクション 5.2 では、関数名と実行時間を含む関数の実行を記録する簡単なデコレータの例を分析します。

この記事は、「クリーンな Python コードの作成」からの抜粋です。

この本は、Python ソフトウェア エンジニアリングの主な実践と原則を紹介し、読者が次のようなコードを作成できるようにすることを目的としています。コードのメンテナンスが容易になり、よりクリーンになります。この本は合計 10 章で構成されており、第 1 章では Python 言語の基本知識と Python 開発環境を構築するために必要な主なツールを紹介し、第 2 章では Python スタイルのコードについて説明し、Python の最初のイディオムを紹介し、第 3 章では Python の原則を要約します。優れたコード 一般的な機能、ソフトウェア エンジニアリングの一般原則を概説します。第 4 章では、オブジェクト指向ソフトウェア設計の一連の原則、つまり SOLID 原則を紹介します。第 5 章では、Python の最もユニークな機能の 1 つであるデコレータを紹介します。第 6 章では、記述について説明します。記述子、記述子を通じてオブジェクトからより多くの情報を取得する方法を紹介、第 7 章と第 8 章ではジェネレータと単体テストとリファクタリングに関する関連コンテンツを紹介、第 9 章では Python の最も一般的な設計パターンを概説、第 10 章ではクリーンなコードが重要であることを再度強調しています。優れたアーキテクチャを実現するための基礎。

この本は、すべての Python プログラミング愛好家、プログラミングに興味のある人、および Python について詳しく知りたいその他のソフトウェア エンジニアリングの実務者に適しています。

関連する無料学習の推奨事項: Python チュートリアル(ビデオ)

以上がPython のデコレータとは何ですか?デコレーターはどのように働くのですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。