谈一谈Python中的装饰器

1、装饰器基础介绍

1.1 何为Python中的装饰器?

Python中装饰器的定义以及用途:

装饰器是一种特殊的函数,它可以接受一个函数作为参数,并返回一个新的函数。装饰器可以用来修改或增强函数的行为,而不需要修改函数本身的代码。在Python中,装饰器通常用于实现AOP(面向切面编程),例如日志记录、性能分析、缓存等。装饰器的语法使用@符号,将装饰器函数放在被装饰函数的定义之前

学过设计模式的朋友都知道,设计模式的结构型模式中也有一个叫装饰器模式,那这个和Python中的装饰器有什么不同呢?

设计模式中的装饰器的定义以及用途:

设计模式中的装饰器是一种结构型模式,它可以在不改变原对象的情况下,为对象添加额外的功能。装饰器模式通常用于在运行时动态地为对象添加功能,而不是在编译时静态地为对象添加功能。装饰器模式通常涉及到多个对象之间的协作,而不是单个函数或对象。

因此,Python中的装饰器和设计模式中的装饰器虽然名称相同,但是它们的实现方式和应用场景有很大的不同。

1.2 闭包

那Python种的装饰器是怎么实现的呢?先不用着急,我们先来一起学习学习Python中的闭包。

那什么叫做闭包呢?

闭包是指一个函数和它所在的环境变量的组合,即在函数内部定义的函数可以访问外部函数的变量和参数,即使外部函数已经返回。闭包可以用来实现函数式编程中的柯里化、惰性求值、函数组合等高级特性。

看着上面的文字,是不是感觉有点抽象。我说一说我对闭包的理解

闭包是由外部函数和内部函数,内部函数引用到了外部函数定义的变量,外部函数的返回值是内部函数的函数名。对于这样的函数,我们就称为闭包。

好像也有点抽象,我们来看一断代码,就能够理解上面的话了。

def my_decorator():  # my_decorator 这个就叫做外部函数a = 1def inner():  # inner 这个叫做内部函数print(a)  # 内部函数引用到了外部函数中定义的变量return inner  # 外部函数的返回值是内部函数名

2、函数装饰器的实现

上面讲解了装饰器的定义、用途,还有闭包,那怎么去实现一个装饰器呢?不急,接下来我们一起来学习如何实现装饰器。

装饰器不是说可以不改变一个函数源代码的基础上,给这个函数添加额外的功能吗?那怎么做呢?

接下来,我们就一起实现一个装饰器,来计算函数的执行时间。Let‘s go!

2.1 不使用@实现装饰器

首先,使用闭包定义一个统计函数执行时间的功能。

def process_time(func):def inner(*args, **kwargs):start_time = time.time()ret = func(*args, **kwargs)end_time = time.time()print("函数的执行时间为:%d" % (end_time-start_time))return retreturn inner

接下来定义一个函数,使用比较来计算函数的执行时间。

import timedef process_time(func):def inner(*args, **kwargs):start_time = time.time()ret = func(*args, **kwargs)end_time = time.time()print("函数的执行时间为:%d" % (end_time-start_time))return retreturn innerdef test(sleep_time):time.sleep(sleep_time)t1 = process_time(test)
t1(1)
print("------------")
t1(2)

执行结果:

函数的执行时间为:1
------------
函数的执行时间为:2

通过上面的代码,我们观察到,我们并没有修改test函数的源代码,依旧给test函数添加上了统计函数执行时间的功能。

Python中实现上述功能,有更加优雅的方式。下面,我们就一起来看看如何实现的。

2.2 Python中使用语法糖的装饰器(推荐使用)

import timedef process_time(func):def inner(*args, **kwargs):start_time = time.time()ret = func(*args, **kwargs)end_time = time.time()print("函数的执行时间为:%d" % (end_time-start_time))return retreturn inner@process_time
def test(sleep_time):time.sleep(sleep_time)test(1)
print("------------")
test(2)

执行结果:

函数的执行时间为:1
------------
函数的执行时间为:2

观察上面的代码变动,发现只有很少的部分修改了。

1、test函数上面添加了一行@process_time

2、test函数的调用方式发生了改变。

其他的并没有发生变化,整个代码看起来也更加清爽了。

提示:

当使用@装饰器时,会自动执行 闭包中的外部函数内容。这个可以自行验证。

当使用@装饰器时,Python解释器为我们做了什么?

当使用@装饰器时,Python解释器会将被装饰的函数作为参数传递给装饰器函数,并将其返回值作为新的函数对象替换原来的函数对象。这样,每次调用被装饰的函数时,实际上是调用了装饰器函数返回的新函数对象。

Python 装饰器 @ 实际上是一种语法糖,它可以让我们在不改变原函数代码的情况下,对函数进行扩展或修改。当我们使用 @ 装饰器时,实际上是将被装饰函数作为参数传递给装饰器函数,然后将装饰器函数的返回值赋值给原函数名。因此,@ 装饰器并不会进行内存拷贝。

通过下面的函数,可以得知,innertest函数指向的是同一个内存地址。

import timedef process_time(func):print("func id --->", id(func))def inner(*args, **kwargs):start_time = time.time()ret = func(*args, **kwargs)end_time = time.time()print("函数的执行时间为:%d" % (end_time - start_time))return retprint("inner id --->", id(inner))return inner@process_time
def test(sleep_time):print("test func id --->", id(test))time.sleep(sleep_time)print("test id --->", id(test))

执行结果:

func id ---> 4312377952
inner id ---> 4313983008
test id ---> 4313983008

使用语法糖时,Python解释器底层为我们做了这样的处理。

2.3 多个装饰器的执行顺序

上面的两个例子,都只有一个装饰器,是不是Python只能写一个装饰器呢。其实不是的。主要是为了讲解简单。接下来,我们一起来看看,多个装饰器的执行顺序。

def outer_1(func):print("coming outer_1")def inner_1():print("coming inner_1")func()return inner_1def outer_2(func):print("coming outer_2")def inner_2():print("coming inner_2")func()return inner_2def outer_3(func):print("coming outer_3")def inner_3():print("coming inner_3")func()return inner_3@outer_1
@outer_2
@outer_3
def test():print("coming test")test()

执行结果:

coming outer_3
coming outer_2
coming outer_1
coming inner_1
coming inner_2
coming inner_3
coming test

outer_3 -> outer_2 -> outer_1 -> inner_1 -> inner_2 -> inner_3 -> 被装饰函数

从上面的执行结果,可以得出如下结论:

使用多个装饰器装饰函数时,
外部函数的执行顺序是从下到上的。
内部函数的执行顺序是从下往上的。

多个装饰器装饰函数时,Python解释器底层做了啥

通过下面这段代码验证

def outer_1(func):print("coming outer_1, func id -->", id(func))def inner_1():print("coming inner_1")func()print("inner_1 id -->", id(inner_1))return inner_1def outer_2(func):print("coming outer_2, func id -->", id(func))def inner_2():print("coming inner_2")func()print("inner_2 id -->", id(inner_2))return inner_2def outer_3(func):print("coming outer_3, func id -->", id(func))def inner_3():print("coming inner_3")func()print("inner_3 id -->", id(inner_3))return inner_3@outer_1
@outer_2
@outer_3
def test():print("coming test")test()

执行结果:

coming outer_3, func id --> 4389102784
inner_3 id --> 4389102928
coming outer_2, func id --> 4389102928
inner_2 id --> 4389103072
coming outer_1, func id --> 4389103072
inner_1 id --> 4389103216
coming inner_1
coming inner_2
coming inner_3
coming test

2.4 带参数的装饰器

该如何实现带参数的装饰器呢,其实原理一样的,我们再定义一个外层函数,外层函数的返回值是内存函数的名称,即引用。

下面我们来看一个例子:

def is_process(flag):def outer_1(func):print("coming outer_1, func id -->", id(func))def inner_1():print("coming inner_1")if flag:func()print("inner_1 id -->", id(inner_1))return inner_1return outer_1@is_process(True)
def test():print("coming test")test()

注意:

  • 我们装饰函数时,装饰器的写法不同了,变成了@is_process(True),这里是调用了is_process这个函数

3、函数装饰器的注意点(wraps函数)

猜一猜下面函数会输出什么?

def outer_1(func):def inner_1():print("inner_1, func __name__", func.__name__)print("inner_1, func __doc__", func.__doc__)func()return inner_1@outer_1
def test():"""this is test"""print("outer_1, func __name__", test.__name__)print("outer_1, func __doc__", test.__doc__)test()

函数执行结果:

inner_1, func __name__ test
inner_1, func __doc__ this is test
test, test __name__ inner_1
test, test __doc__ None

注意到没,在test函数体内打印函数的 __name__、__doc__ 属性,居然变成内部函数的了。

这个是为什么呢?

Python装饰器在装饰函数时,会将原函数的函数名、文档字符串、参数列表等属性复制到装饰器函数中,但是装饰器函数并不会复制原函数的所有属性。例如,原函数的name属性、doc属性、module属性等都不会被复制到装饰器函数中。

为了避免这种情况,可以使用functools库中的wraps装饰器来保留原来函数对象的属性。wraps装饰器可以将原来函数对象的属性复制到新的函数对象中,从而避免属性丢失的问题。

from functools import wrapsdef outer_1(func):@wraps(func)def inner_1():print("inner_1, func __name__", func.__name__)print("inner_1, func __doc__", func.__doc__)func()return inner_1@outer_1
def test():"""this is test"""print("test, test __name__", test.__name__)print("test, test __doc__", test.__doc__)test()

执行结果:

inner_1, func __name__ test
inner_1, func __doc__ this is test
test, test __name__ test
test, test __doc__ this is test

4、类装饰器

上面我们都是使用的函数来实现装饰器的功能,那可不可以用类来实现装饰器的功能呢?我们知道函数实现装饰器的原理是外部函数的参数是被装饰的函数,外部函数返回内部函数的名称。内部函数中去执行被装饰的函数。

那么其实类也是可以用来实现装饰器的,因为当我们为 类 定义了 __call__方法时,这个类就成了可调用对象,实例化后可直接调用。

class ProcessTime:def __call__(self, *args, **kwargs):print("call")p = ProcessTime()
p()

4.1 类装饰器的实现

import timeclass ProcessTime:def __init__(self, func):print("coming ProcessTime __init__")self.func = funcdef __call__(self, *args, **kwargs):start_time = time.time()print("coming ProcessTime __call__, id(self.func) -->", id(self.func))ret = self.func(*args, **kwargs)end_time = time.time()print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))return ret@ProcessTime
def test(sleep_time):time.sleep(sleep_time)return "tet"test(1)

执行结果:

coming ProcessTime __init__
coming ProcessTime __call__, id(self.func) --> 4488922160
ProcessTime 函数的执行时间为:1

通过上面的执行结果,我们可以得到,@ProcessTime的作用是 test = ProcessTime(test)。又因为 ProcessTime定义了__call__方法,是可调用对象,所以可以像函数那样直接调用实例化ProcessTime后的对象。

这里可以验证,通过注释掉装饰器,手动初始化ProcessTime类。得到的结果是一样的。

# @ProcessTime
def test(sleep_time):time.sleep(sleep_time)return "tet"test = ProcessTime(test)
test(1)

4.2 多个类装饰器的执行顺序

多个类装饰器的执行顺序是怎么样的呢,这里我们也通过代码来进行验证。

import timeclass ProcessTime:def __init__(self, func):print("coming ProcessTime __init__", id(self))self.func = funcdef __call__(self, *args, **kwargs):start_time = time.time()print("coming ProcessTime __call__, id(self.func) -->", id(self.func))ret = self.func(*args, **kwargs)end_time = time.time()print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))return retclass ProcessTime2:def __init__(self, func):print("coming ProcessTime2 __init__", id(self))self.func = funcdef __call__(self, *args, **kwargs):start_time = time.time()print("coming ProcessTime2 __call__, id(self.func) -->", id(self.func))ret = self.func(*args, **kwargs)end_time = time.time()print("ProcessTime2 函数的执行时间为:%d" % (end_time - start_time))return ret@ProcessTime
@ProcessTime2
def test(sleep_time):time.sleep(sleep_time)return "tet"# test = ProcessTime2(test)
# test = ProcessTime(test)t = test(1)

执行结果:

coming ProcessTime2 __init__ 4472235104
coming ProcessTime __init__ 4473162672
coming ProcessTime __call__, id(self.func) --> 4472235104
coming ProcessTime2 __call__, id(self.func) --> 4471735344
ProcessTime2 函数的执行时间为:1
ProcessTime 函数的执行时间为:1

从上面的结果,我们得到,执行顺序是:

ProcessTime2 中的__init__ -> ProcessTime 中的__init__ -> ProcessTime 中的__call__ -> ProcessTime2 中的__call__

特别注意:

ProcessTime 中的__call__ 中的代码并不会执行完后再去执行 ProcessTime2 中的__call__,而是在调用 ret = self.func(*args, **kwargs) 方法后,就回去执行 ProcessTime2 中的__call__的代码。

4.3 类装饰器存在的问题

其实,类装饰器也存在和函数装饰器一样的问题。它会覆盖原函数的元数据信息,例如函数名、文档字符串、参数列表等。这可能会导致一些问题,例如调试时无法正确显示函数名、文档生成工具无法正确生成文档等。

import time
from functools import wrapsclass ProcessTime:def __init__(self, func):print("coming ProcessTime __init__", id(self))self.func = funcdef __call__(self, *args, **kwargs):start_time = time.time()print("coming ProcessTime __call__, id(self.func) -->", id(self.func))ret = self.func(*args, **kwargs)end_time = time.time()print("ProcessTime 函数的执行时间为:%d" % (end_time - start_time))return ret@ProcessTime
def test(sleep_time):"tets"print("test.__doc__", test.__doc__)# print(test.__name__)  --> 报错,AttributeError: 'ProcessTime' object has no attribute '__name__'time.sleep(sleep_time)return "tet"t = test(1)

那类装饰器该如何解决呢?

我现在还不知道该如何处理,如果有知道的朋友,请不吝赐教,十分感谢!!

5、多个装饰器的执行顺序总结

其实,我觉得不用特别的去记多个装饰器的执行顺序是如何的,我们最重要的是理解到装饰器的执行逻辑是如何的。函数装饰器和类装饰器的初始化顺序都是一样的:从靠近被装饰的函数开始执行初始化操作。把这个核心原理理解到后,多个装饰器的执行顺序在使用的时候,就很容易得到了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/24273.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVM深入 —— JVM的体系架构

前言 能否真正理解JVM的底层实现原理是进阶Java技术的必由之路,Java通过JVM虚拟机的设计使得Java的延拓性更好,平台无关性是其同时兼顾移动端和服务器端开发的重要特性。在本篇文章中,荔枝将会仔细梳理JVM的体系架构和理论知识,希…

Dubbo+Zookeeper使用

说明:Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,官方提供了 Java、Golang 等多语言 SDK 实现。 本文介绍Dubbo的简单使用及一些Dubbo功能特性,注册中心使用的是ZooKeeper,可在…

驱动工作原理

驱动原理 在Linux操作系统中,硬件驱动程序中实现对硬件直接操作,而用户空间,通过通用的系统调用接口(open() 打开相应的驱动设备,ioctl()控制相应的功能等),实现对硬件操作,应用程序没有直接操作…

树的层次遍历

层次遍历简介 广度优先在面试里出现的频率非常高,整体属于简单题。而广度优先遍历又叫做层次遍历,基本过程如下: 层次遍历就是从根节点开始,先访问根节点下面一层全部元素,再访问之后的层次,类似金字塔一样…

【Uniapp 的APP热更新】

Uniapp 的APP热更新功能依赖于其打包工具 HBuilder,具体步骤如下: 1. 在 HBuilder 中构建并打包出应用程序 具体步骤: 1.点击发行,点击制作wgt包 2.根据需求修改文件储存路径和其他配置,点击确定 3.等待打包完成&a…

Rust中的高吞吐量流处理

本篇文章主要介绍了Rust中流处理的概念、方法和优化。作者不仅介绍了流处理的基本概念以及Rust中常用的流处理库,还使用这些库实现了一个流处理程序。 最后,作者介绍了如何通过测量空闲和阻塞时间来优化流处理程序的性能,并将这些内容同步至…

Android 实现账号诊断动画效果,逐条检测对应的项目

Dialog中的项目 逐条检测效果&#xff1a; 依赖库&#xff1a; implementation com.github.li-xiaojun:XPopup:2.9.19 implementation com.blankj:utilcodex:1.31.1 implementation com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.101、item_account_check.xml <…

PictureBox基本使用

作用&#xff1a;展示图片&#xff0c;同时也具有click属性&#xff0c;可用充当按钮功能。 常用属性&#xff1a; 设置图片 设置图片的填充模式 常用事件&#xff1a; 后台代码示范&#xff1a;增加点击事件 private void pictureBox1_Click(object sender, EventArgs e){//…

【CodeWhisperer】亚马逊版代码生成工具

大家好&#xff0c;我是荷逸&#xff0c;今天给大家带来的是代码生成工具【CodeWhisperer】 CodeWhisperer简介 CodeWhisperer是亚⻢逊出品的一款基于机器学习的通用代码生成器&#xff0c;可实时提供代码建议。 在编写代码时&#xff0c;它会自动根据我们现有的代码和注释生…

Java中「Future」接口详解

一、背景 在系统中&#xff0c;异步执行任务&#xff0c;是很常见的功能逻辑&#xff0c;但是在不同的场景中&#xff0c;又存在很多细节差异&#xff1b; 有的任务只强调「执行过程」&#xff0c;并不需要追溯任务自身的「执行结果」&#xff0c;这里并不是指对系统和业务产…

JDK, JRE和JVM之间的区别和联系

JDK, JRE和JVM是与Java编程语言相关的三个重要的概念&#xff0c;它们分别代表Java Development Kit&#xff08;Java开发工具包&#xff09;、Java Runtime Environment&#xff08;Java运行时环境&#xff09;和Java虚拟机&#xff08;Java Virtual Machine&#xff09;。它们…

大数据课程G2——Hbase的基本架构

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Hbase的基本架构; ⚪ 掌握Hbase的读写流程; ⚪ 掌握Hbase的设计与优化; 一、基本架构 1. HRegion 1. 在HBase中,会将一个表从行键方向上进行切分,切分成1个或者多个HRegion。 …

C#利用自定义特性以及反射,来提大型项目的开发的效率

在大型项目的开发过程中&#xff0c;需要多人协同工作&#xff0c;来加速项目完成进度。 比如一个软件有100个form&#xff0c;分给100个人来写&#xff0c;每个人完成自己的Form.cs的编写之后&#xff0c;要在Mainform调用自己写的Form。 如果按照正常的Form form1 new For…

MIT 6.824 -- MapReduce -- 01

MIT 6.824 -- MapReduce -- 01 引言抽象和实现可扩展性可用性(容错性)一致性MapReduceMap函数和Reduce函数疑问 课程b站视频地址: MIT 6.824 Distributed Systems Spring 2020 分布式系统 推荐伴读读物: 极客时间 – 大数据经典论文解读DDIA – 数据密集型应用大数据相关论文…

【具身智能】系列论文解读(CoWs on PASTURE VoxPoser Relational Pose Diffusion)

0. My Conclusion CoWs on PASTURE&#xff1a; 擅长零样本的视觉语言对象导航&#xff0c;主要解决了LLM辅助下的任务级动作执行任务VoxPoser&#xff1a; 擅长设计一些未预定义的动作轨迹&#xff0c;主要解决了LLM辅助下的动作轨迹设计任务Relational Pose Diffusion&#…

Packet Tracer - 将路由器连接到 LAN

Packet Tracer - 将路由器连接到 LAN 地址分配表 设备 接口 IP 地址 子网掩码 默认网关 R1 G0/0 192.168.10.1 255.255.255.0 N/A G0/1 192.168.11.1 255.255.255.0 N/A S0/0/0 (DCE) 209.165.200.225 255.255.255.252 N/A R2 G0/0 10.1.1.1 255.255.255…

概率论与数理统计复习总结3

概率论与数理统计复习总结&#xff0c;仅供笔者复习使用&#xff0c;参考教材&#xff1a; 《概率论与数理统计》/ 荣腾中主编. — 第 2 版. 高等教育出版社《2024高途考研数学——概率基础精讲》王喆 概率论与数理统计实际上是两个互补的分支&#xff1a;概率论 在 已知随机…

Kernel Exception导致手机重启案例分析

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、高温触发 Kernel Exception 重启问题二、解决方案三、提高电池温度方案 一、 高温触发 Kernel Exception 重启问题 手机 电池温度 默认60度以上高温…

CBCGPRibbon 添加背景图片

resource.h中声明资源的ID&#xff1a;ID_RIBBON_BACKIMAGE rc文件中添加png图片路径&#xff1a; ID_RIBBON_BACKIMAGE PNG DISCARDABLE "res\\bkribbon.png" 代码中添加下测&#xff1a; //添加背景图片 m_wndRibbonBar.SetBackgroundImage(ID_RIB…

C语言单链表OJ题(较易)

一、移除链表元素 leetcode链接 题目描述&#xff1a; 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 思路&#xff1a; 正常遍历&#xff0c;找到value的值与题目中相同的结点去fr…