>  기사  >  백엔드 개발  >  Python의 데코레이터란 무엇입니까? 데코레이터는 어떻게 작동하나요?

Python의 데코레이터란 무엇입니까? 데코레이터는 어떻게 작동하나요?

coldplay.xixi
coldplay.xixi앞으로
2021-03-08 09:49:262665검색

Python의 데코레이터란 무엇입니까? 데코레이터는 어떻게 작동하나요?

Python은 초기 정의 이후 수정해야 하는 함수와 메서드를 정의하는 방식을 단순화하기 위한 메커니즘으로 PEP-318에서 아주 초기에 데코레이터를 도입했습니다.

이 작업을 수행하는 원래 동기 중 하나는 classmethod 및 staticmethod와 같은 함수를 사용하여 메서드의 원래 정의를 변환하는 것이었지만 함수의 초기 정의를 수정하려면 추가 코드 줄이 필요합니다.

일반적으로 함수에 변환을 적용해야 할 때마다 수정자 함수를 사용하여 함수를 호출한 다음 함수가 처음 정의되었을 때 이름에 다시 할당해야 합니다.

예를 들어, 원본이라는 함수가 있고 그 위에 원래 동작을 변경하는 함수(수정자라고 함)가 있다고 가정하면 다음과 같이 작성해야 합니다.

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

(관련 무료 학습 권장 사항: python 비디오 자습서 )

함수를 어떻게 변경하고 동일한 이름으로 다시 할당했는지 주목하세요. 이는 혼란스럽고 오류가 발생하기 쉬우며(누군가가 함수를 재할당하는 것을 잊어버렸거나 함수를 재할당했지만 함수 정의 다음 줄이 아니라 다른 어딘가에 있다고 가정할 때) 번거롭습니다. 이러한 이유로 Python 언어에는 일부 구문 지원이 추가되었습니다.

이전 예제는 다음과 같이 다시 작성할 수 있습니다.

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

이는 데코레이터가 데코레이터 자체에 대한 첫 번째 인수로 호출된 후에 오는 내용에 대한 구문 설탕일 뿐이며 결과는 데코레이터가 반환하는 것임을 의미합니다.

Python의 용어와 일관성을 유지하기 위해 이 예에서 modifier는 데코레이터라고 하며, 원본은 데코레이션 함수로, 종종 래퍼 객체라고도 합니다.

이 기능은 원래 메소드 및 함수용으로 생각되었지만 실제 구문에서는 모든 유형의 객체를 꾸밀 수 있으므로 함수, 메소드, 생성기 및 클래스에 적용되는 데코레이터를 살펴보겠습니다.

마지막으로 주목해야 할 점은 데코레이터의 이름은 정확하지만(결국 데코레이터는 실제로 래핑된 함수를 변경, 확장 또는 처리하고 있음) 데코레이터 디자인 패턴과 혼동하지 마세요.

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 데코레이터는 매개변수를 받지 않으므로 아래와 같이 어떤 함수에도 쉽게 적용할 수 있습니다.

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

처음에 설명했듯이 run_Operation 상단의 @retry 정의는 Python에서 제공하는 구문 설탕일 뿐입니다. 실제로 run_Operation = retry(run_Operation)을 실행하는데 사용됩니다.

이 제한된 예에서는 데코레이터를 사용하여 특정 조건(이 예에서는 가능한 시간 초과 관련 예외로 표현됨)에서 데코레이팅된 코드에 대한 여러 호출을 허용하는 일반 재시도 작업을 만드는 방법을 볼 수 있습니다.

5.1.2 데코레이션 클래스

클래스도 데코레이션이 가능하며(PEP-3129) 데코레이션 방법은 구문 함수와 동일합니다. 유일한 차이점은 데코레이터에 대한 코드를 작성할 때 수신되는 것이 함수가 아니라 클래스라는 점을 고려해야 한다는 것입니다.

일부 실무자는 클래스를 꾸미는 것이 꽤 복잡한 일이라고 생각할 수 있으며, 그러한 시나리오는 클래스에서 일부 속성과 메서드를 선언하기 때문에 가독성에 해를 끼칠 수 있지만, 무대 뒤에서 데코레이터가 일부 변경 사항을 적용할 수 있습니다. 전혀 다른 수업을 선보입니다.

이 평가는 정확하지만 장식 기술이 심각하게 남용되는 경우에만 적용됩니다. 객관적으로 이는 함수 꾸미기와 다르지 않습니다. 결국 클래스는 함수와 마찬가지로 Python 생태계의 객체 유형 중 하나일 뿐입니다. 이 문제의 장단점을 섹션 5.4에서 다시 살펴보겠지만 여기서는 데코레이터의 장점, 특히 클래스에 적용되는 장점만 살펴보겠습니다.

(1) 코드 재사용과 DRY 원칙의 모든 이점. 클래스 데코레이터의 유용한 사례는 여러 클래스가 특정 인터페이스나 표준을 따르도록 하는 것입니다(여러 클래스에 적용될 데코레이터에서 단 한 번의 확인만 수행하여).

(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 사용량 또는 메모리 사용 공간)을 모니터링합니다. 함수의 실행 시간을 측정합니다. ;

(4) 함수가 호출될 때와 함수에 전달된 매개변수를 기록합니다.

함수 이름과 실행 시간을 포함하여 함수 실행을 기록하는 간단한 데코레이터 예제를 섹션 5.2에서 분석하겠습니다.

이 기사는 "깨끗한 Python 코드 작성"에서 발췌되었습니다.

이 책은 독자가 유지 관리하기 쉽고 깔끔한 코드를 작성하는 데 도움을 주기 위해 Python 소프트웨어 엔지니어링의 주요 사례와 원칙을 소개합니다. 이 책은 총 10장으로 구성되어 있습니다. 1장은 Python 언어에 대한 기본 지식과 Python 개발 환경을 구축하는 데 필요한 주요 도구를 소개하고, 2장은 Python 스타일 코드를 설명하고 Python의 첫 번째 관용어를 소개합니다. 좋은 코드 일반 기능, 소프트웨어 엔지니어링의 일반 원칙을 검토합니다. 4장에서는 객체 지향 소프트웨어 설계에 대한 일련의 원칙, 즉 SOLID 원칙을 소개합니다. 5장에서는 Python의 가장 독특한 기능 중 하나인 데코레이터를 소개합니다. 설명자를 통해 객체에서 더 많은 정보를 얻는 방법을 소개합니다. 7장과 8장은 단위 테스트 및 리팩토링에 대한 생성기와 관련 내용을 소개합니다. 10장에서는 깔끔한 코드가 가장 중요하다는 점을 다시 한 번 강조합니다. 좋은 아키텍처를 달성하기 위한 기반.

이 책은 모든 Python 프로그래밍 애호가, 프로그래밍에 관심이 있는 사람, Python에 대해 더 자세히 알고 싶은 기타 소프트웨어 엔지니어링 실무자에게 적합합니다.

관련 무료 학습 권장 사항:

python 튜토리얼(동영상)

위 내용은 Python의 데코레이터란 무엇입니까? 데코레이터는 어떻게 작동하나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제