首页 后端开发 Python教程 Python中的装饰器是什么?装饰器是如何工作的?

Python中的装饰器是什么?装饰器是如何工作的?

Mar 08, 2021 am 09:49 AM
python 装饰器

Python中的装饰器是什么?装饰器是如何工作的?

Python很早就引入了装饰器——在PEP-318中,作为一种简化函数和方法定义方式的机制,这些函数和方法在初始定义之后必须进行修改。

这样做的最初动机之一是,使用classmethod和staticmethod等函数来转换方法的原始定义,但是它们需要额外的一行代码来修改函数的初始定义。

一般来说,每次必须对函数应用转换时,我们必须使用modifier函数调用它,然后将它重新分配到函数初始定义时的名称中。

例如,假设有一个叫作original的函数,在它上面有一个改变original行为的函数(叫作modifier),那么我们必须这样写:

def original(...):
    ...
original = modifier(original)
登录后复制

(相关免费学习推荐:python视频教程

请注意我们是如何更改函数并将其重新分配到相同的名称中去的。这是令人困惑的,很容易出错(假设有人忘记重新分配函数,或者重新分配了函数,但不在函数定义之后的行中,而是在更远的地方),而且很麻烦。出于这个原因,Python语言增加了一些语法支持。

前面的示例可以改写为如下样式:

@modifier
def original(...):
   ...
登录后复制

这意味着装饰器只是语法糖,用于调用装饰器之后的内容作为装饰器本身的第一个参数,结果将是装饰器返回的内容。

为了与Python的术语一致,在我们的示例中modifier称为装饰器,original是装饰函数,通常也被称为包装对象。

虽然该功能最初被认为是用于方法和函数的,但实际的语法允许它修饰任何类型的对象,因此我们将研究应用于函数、方法、生成器和类的装饰器。

最后一点需要注意的是,虽然装饰器的名称是正确的(毕竟,装饰器实际上是在对包装函数进行更改、扩展或处理),但不要将它与装饰器设计模式混淆。

5.1.1 装饰器函数

函数可能是对可以装饰的Python对象的最简单的表示形式。我们可以在函数上使用装饰器来应用各种逻辑——我们可以验证参数、检查前置条件、完全改变行为、修改其签名、缓存结果(创建原始函数的内存版本)等。

例如,我们将创建一个实现retry机制的基本装饰器,控制一个特定的域级异常并重试一定的次数:

# 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)。

在这个有限的示例中,我们可以看到如何用装饰器创建一个通用的retry操作,在某些确定的条件下(在本示例中,表示为可能与超时相关的异常),该操作将允许多次调用装饰后的代码。

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使用量或内存占用);

(3)测量函数的运行时间;

(4)函数被调用时的日志,以及传递给它的参数。

我们将在5.2节剖析一个简单的装饰器示例,该示例记录了函数的执行情况,包括函数名和运行时间。

本文摘自《编写整洁的Python代码》 

本书介绍Python软件工程的主要实践和原则,旨在帮助读者编写更易于维护和更整洁的代码。全书共10章:第1章介绍Python语言的基础知识和搭建Python开发环境所需的主要工具;第2章描述Python风格代码,介绍Python中的第一个习惯用法;第3章总结好代码的一般特征,回顾软件工程中的一般原则;第4章介绍一套面向对象软件设计的原则,即SOLID原则;第5章介绍装饰器,它是Python的**特性之一;第6章探讨描述符,介绍如何通过描述符从对象中获取更多的信息;第7章和第8章介绍生成器以及单元测试和重构的相关内容;第9章回顾Python中最常见的设计模式;第10章再次强调代码整洁是实现良好架构的基础。

本书适合所有Python编程爱好者、对程序设计感兴趣的人,以及其他想学习更多Python知识的软件工程的从业人员。

相关免费学习推荐:python教程(视频)

以上是Python中的装饰器是什么?装饰器是如何工作的?的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
威尔R.E.P.O.有交叉游戏吗?
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

PHP和Python:代码示例和比较 PHP和Python:代码示例和比较 Apr 15, 2025 am 12:07 AM

PHP和Python各有优劣,选择取决于项目需求和个人偏好。1.PHP适合快速开发和维护大型Web应用。2.Python在数据科学和机器学习领域占据主导地位。

CentOS上PyTorch的GPU支持情况如何 CentOS上PyTorch的GPU支持情况如何 Apr 14, 2025 pm 06:48 PM

在CentOS系统上启用PyTorchGPU加速,需要安装CUDA、cuDNN以及PyTorch的GPU版本。以下步骤将引导您完成这一过程:CUDA和cuDNN安装确定CUDA版本兼容性:使用nvidia-smi命令查看您的NVIDIA显卡支持的CUDA版本。例如,您的MX450显卡可能支持CUDA11.1或更高版本。下载并安装CUDAToolkit:访问NVIDIACUDAToolkit官网,根据您显卡支持的最高CUDA版本下载并安装相应的版本。安装cuDNN库:前

Python vs. JavaScript:社区,图书馆和资源 Python vs. JavaScript:社区,图书馆和资源 Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

docker原理详解 docker原理详解 Apr 14, 2025 pm 11:57 PM

Docker利用Linux内核特性,提供高效、隔离的应用运行环境。其工作原理如下:1. 镜像作为只读模板,包含运行应用所需的一切;2. 联合文件系统(UnionFS)层叠多个文件系统,只存储差异部分,节省空间并加快速度;3. 守护进程管理镜像和容器,客户端用于交互;4. Namespaces和cgroups实现容器隔离和资源限制;5. 多种网络模式支持容器互联。理解这些核心概念,才能更好地利用Docker。

minio安装centos兼容性 minio安装centos兼容性 Apr 14, 2025 pm 05:45 PM

MinIO对象存储:CentOS系统下的高性能部署MinIO是一款基于Go语言开发的高性能、分布式对象存储系统,与AmazonS3兼容。它支持多种客户端语言,包括Java、Python、JavaScript和Go。本文将简要介绍MinIO在CentOS系统上的安装和兼容性。CentOS版本兼容性MinIO已在多个CentOS版本上得到验证,包括但不限于:CentOS7.9:提供完整的安装指南,涵盖集群配置、环境准备、配置文件设置、磁盘分区以及MinI

CentOS上PyTorch的分布式训练如何操作 CentOS上PyTorch的分布式训练如何操作 Apr 14, 2025 pm 06:36 PM

在CentOS系统上进行PyTorch分布式训练,需要按照以下步骤操作:PyTorch安装:前提是CentOS系统已安装Python和pip。根据您的CUDA版本,从PyTorch官网获取合适的安装命令。对于仅需CPU的训练,可以使用以下命令:pipinstalltorchtorchvisiontorchaudio如需GPU支持,请确保已安装对应版本的CUDA和cuDNN,并使用相应的PyTorch版本进行安装。分布式环境配置:分布式训练通常需要多台机器或单机多GPU。所

CentOS上PyTorch版本怎么选 CentOS上PyTorch版本怎么选 Apr 14, 2025 pm 06:51 PM

在CentOS系统上安装PyTorch,需要仔细选择合适的版本,并考虑以下几个关键因素:一、系统环境兼容性:操作系统:建议使用CentOS7或更高版本。CUDA与cuDNN:PyTorch版本与CUDA版本密切相关。例如,PyTorch1.9.0需要CUDA11.1,而PyTorch2.0.1则需要CUDA11.3。cuDNN版本也必须与CUDA版本匹配。选择PyTorch版本前,务必确认已安装兼容的CUDA和cuDNN版本。Python版本:PyTorch官方支

CentOS上如何更新PyTorch到最新版本 CentOS上如何更新PyTorch到最新版本 Apr 14, 2025 pm 06:15 PM

在CentOS上更新PyTorch到最新版本,可以按照以下步骤进行:方法一:使用pip升级pip:首先确保你的pip是最新版本,因为旧版本的pip可能无法正确安装最新版本的PyTorch。pipinstall--upgradepip卸载旧版本的PyTorch(如果已安装):pipuninstalltorchtorchvisiontorchaudio安装最新

See all articles