python小课堂29 - 进阶必修之装饰器
off999 2024-12-03 00:12 15 浏览 0 评论
前言
装饰器(Decorators)是 Python 的一个重要部分,通俗的说:它们是修改其他函数的功能的函数,使用装饰器有助于让代码更简短,也更 Pythonic(Python范儿)。大多数初学者不知道在哪儿使用它们,跟随笔者的步伐来看下装饰器如何使用吧!
PS:装饰器和闭包一样,并不是说少了装饰器对于Python编写代码来说就不能完成相应的需求功能了,装饰器是在编程中的设计模式中抽象出来的一个重要思想,许多框架都使用到了装饰器。
装饰器的定义
在这里,笔者首先不会直接给出装饰器的定义,因为它本身的含义就很抽象,并不像上一次介绍的高阶函数,了解入参和结果就能使用了。依然遵循老规矩,采用 场景 + 示例 的模式一步步推导出装饰器的定义来。
场景1:假设现在有一个函数,这个函数里面什么也不干,仅仅是让它休眠2秒,现在的需求是,如何才能不修改原有函数的基础上,直接对原来的函数增加一个打印耗时的功能呢?(实际上这是写出良好代码的设计原则,之前有提到过,开闭原则,对修改是封闭的,对拓展是开放的,非专业人员混个眼熟啦~)思考尝试想一下或者写一下试试,再看下面的程序:
import time def sleep_func(): time.sleep(2) def time_spend_func(func): before = time.time() print(f"开始执行前时间:{before}") func() after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s') time_spend_func(sleep_func)
推导步骤1:
讲解一下上面的程序,使用 Python 自带的 time 模块,我们可以调用 sleep 方法让程序休眠,参数是以秒为单位,可以自行 Ctrl 点进去看下官方注释。
在场景设定下,不修改原来的代码,想看原来的函数一共耗时多少,所以可以新增一个专门计算耗时的函数,姑且称之为 time_spend_func 。在其里面使用 time.time() 方法可以得到当前时间的时间戳形式。(时间戳 timestamp ,一个能表示一份数据在某个特定时间之前已经存在的、 完整的、 可验证的数据,通常是一个字符序列,唯一地标识某一刻的时间。)
可以看到最后调用新增程序时,笔者将原有函数以参数的形式传入了计算耗时的函数中(这点在闭包时已经介绍过了,详见小课堂26)。传入原有函数的目的,是为了在新增函数中调用原有函数的逻辑,也就是睡上 2s。
最终耗时的具体时间,通过时间戳的数字进行减法得到的是 float 型,将之转为数字整型即可显示最终耗时结果。
这样的实现方法是没有对原有函数进行修改的,虽然确实符合场景设定,但是它本身是存在缺点的!大家可以想一下,新增函数所实现的功能确实属于新增功能,但这个功能原应该“依赖”于原有函数,这句话怎么理解呢?消耗时间这个动作是属于原有函数本身的,而现在的新增函数是在调用原有函数的前后强制新增了一段记录时间的代码。
如果还不能理解,来看下上面的写法和下面的写法有什么不同吗?
def sleep_func(): time.sleep(2) before = time.time() print(f"开始执行前时间:{before}") sleep_func() after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s')
无非就是取消了函数的写法,强制在调用原函数的前后加上了时间的逻辑!
好好体会一下,明白了以后,我们可以通过装饰器来解决这一缺点,使新增功能原本就属于原有函数,继续看推导2。
推导步骤2:
接下来看下 Python 中的装饰器如何书写呢?
def decorators(func): def wrapper(): before = time.time() print(f"开始执行前时间:{before}") func() after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s') return wrapper def sleep_func(): time.sleep(2) f = decorators(sleep_func) f()
先仔细看一下代码,decorator 是装饰器的意思,wrapper 是包装的意思。看完代码的同学,乍一看结构是不是与之前提到的闭包结构非常相似!只是少了一个所谓的“环境变量”....
解释一下这段代码,定义一个外层函数 decorator 装饰器,内层函数定义一个 wrapper 的包装函数,其中外层函数接受外界传入的函数作为参数,并且在内部函数 wrapper 中进行调用,而其余的打印耗时的逻辑依然没有修改。
这么一看,肯定有人会有疑问,这样的写法意义在哪里?最终调用还是和推导1一样,调用装饰器函数,并且将具体的休眠函数作为参数传入,最后还多出一步f的调用。没错,如果你想到了这里,恭喜你,说明走心了!这样的写法确实不是最完美的,笔者写到这里仅仅是定义了一个装饰器而已,但是并没有使之起到装饰的作用,下面继续看推导3。
推导步骤3:
有了前两步的推导,第三步就是要介绍一下Python的语法糖了!
语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言:操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。
在Python中,通过 @ 符号来表示装饰器!而 @ 则是Python的语法糖之一,具体用法如下,终极装饰器的示例:
def decorators(func): def wrapper(): before = time.time() print(f"开始执行前时间:{before}") func() after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s') return wrapper @decorators def sleep_func(): time.sleep(2) sleep_func()
解释一下代码,在推导2的基础上,只需要将 @装饰器函数的名称 加在原有函数的“头上”,便可完成所谓的装饰器装饰,最终调用原有函数即可执行。看到这样的写法,知道为什么叫装饰器了吧~因为是装饰在原有函数的“头上”来做装饰物的呀![罒ω罒]......
了解Java的人可能见过这样的写法,这种写法在Java中被叫做注解。但是意义却与Python中的装饰器截然不同。
推导了三个步骤,最终带出了装饰器的定义,大家有没有看出来最终装饰器的好处呢?下面来说明一下:
1. 使用类似闭包的方式来定义装饰器,通过 @ + 装饰器名称 来装饰已有函数。
2. 装饰器的好处,符合了开闭原则,不用修改原有代码,只需新增。
3. 终极装饰器的写法,不会改变原有函数的调用,原来怎么调用,加上装饰器后依然可以使用原来的方法调用,此处指 sleep_func() 的调用。
4. 装饰器还起到了代码复用的作用,因为一个装饰器是可以修饰多个函数的。
装饰器多参数
在上面提到了装饰器还有代码复用的作用,来看下具体示例就会明白为什么这么说了,场景如下:
在推导1~3的场景下,假设我们的 sleep_func 函数休眠时间是由外界传入的,而非固定值,如何修改下推导3中的装饰器呢?
@decorators def sleep_func(seconds): time.sleep(seconds) print(f'休眠函数休眠时间:{seconds}')
修改休眠函数,如上,然后直接调用,来看下结果:
可以看到,报错啦,错误信息大概意思是 wrapper() 必须给定一个参数,因为我们调用的函数传入了一个参数3秒,所以在装饰器中的内层函数需与之同步,修改为如下代码:
def decorators(func): def wrapper(seconds): before = time.time() print(f"开始执行前时间:{before}") func(seconds) after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s') return wrapper @decorators def sleep_func(seconds): time.sleep(seconds) print(f'休眠函数休眠时间:{seconds}') sleep_func(3)
在 wrapper 函数以及内部调用的 func 两处同步加入 seconds 形参,再次调用:
但是这样的写法太死板了,一点也不灵活,假如在 sleep_func 函数中,业务逻辑变了,其中有多个参数传递,那装饰器中岂不是也要修改无数次了?而且每对应一个函数,不同参数个数就需要新建一个装饰器,何谈代码的复用?!
所以,多参数的情况依然可以进行完善,使之达到一个通用的状态。这里要普及一个Python小知识点了,知道这两个小知识点后对后续写出灵活性代码非常有用!
*args 和 **kwargs 的妙用
举两个小例子,一看便了解Python中这两个写法的用途了!
1. *args 用法
def print_words(*args): print(type(args)) for word in args: print(word) print_words(1, '呵呵', True, 1.02333)
在定义函数时,可以使用 *args 作为形参,代表着可以接受无穷多个参数,以上面为例,可以看到在定义得打印函数中,调用时传入了各种类型的参数,最终通过 for 循环遍历打印,可见 *args 本身类型是一个 tuple 类型。需要注意的是,这里的写法 * 是必须的,args 作为形参名称,可修改。
2. **kwargs 用法
def print_words(**kwargs): print(type(kwargs)) print(kwargs) print_words(a=1, e='呵呵', f=True, b=1.02333)
**kwargs,之前的基础篇讲过关键词参数,详见:小课堂16。
而此种写法作为形参传入,最终实参调用时,传入到函数内部解析之后其实就是字典形式,关键词作为字典的key,值则作为 value。
3. *args 和 **kwargs 混合使用
def print_words(*args, **kwargs): print(type(args)) print(args) print(type(kwargs)) print(kwargs) print_words(1, 23, 4, '----分割线----', a=1, e='呵呵', f=True, b=1.02333)
混合使用,效果更佳!这样定义函数的形参非常灵活,可以看到许多第三方库的底层都是利用了这样的写法。。思路拉回到正题,认识到这种写法的好处,将之用到我们的装饰器中去。
装饰器最终灵活版
用到我们的装饰器中去。示例代码如下:
def decorators(func): def wrapper(*args, **kwargs): before = time.time() print(f"开始执行前时间:{before}") func(*args, **kwargs) after = time.time() print(f'开始执行后时间:{after}') print(f'耗时时间:{int(after - before)}s') return wrapper @decorators def sleep_func(seconds, a, b, **kwargs): time.sleep(seconds) print(f'休眠函数休眠时间:{seconds}') print(a, b) print(kwargs) sleep_func(3, '我是a', '我是b', c='我是{"c":"c"}', d='我是{"d":"d"}')
注意标记红框的位置,在调用 func 时,传入的都是带星号的形参,只有使用时才将星号去掉!不要记混啦,建议多多练习!写到这里,最终完美版的装饰器也就诞生了,整篇下来通过一步步的推导将装饰器梳理了一遍!在说最后一句,什么叫做可以复用的装饰器,在上面的基础上在多加一个新的函数,使用相同的装饰器:
@decorators def sleep_func_new(words): print(words) time.sleep(1) sleep_func_new('调用了sleep_func_new')
总结
感觉一写技术文章,不知不觉就超长了.....又是一篇非常详细,非常长的文章,目测上千字啦,学到这里,关于Python的基础与进阶基本上告离段落了,大家看了笔者的文章有木有成长一些呢?从开文到现在,无论是从文字功底还是文字的排版,对于笔者而言都有不少进步,通过各大平台推广,每天公众号的后台人数也在一点点增加,只要有读者看,就是继续写下去的最大动力!
接下来的文章跟实战挂钩了,将之前所有知识点串联在一起最好的方式就是通过实战去训练,而学习Python最有趣的就是关于爬虫的知识了!下一章通过爬虫带你快速入门,串联所有学过的知识以及网络信息采集的开篇!敬请期待....
有想交流Python的同学,欢迎关注公号:migezatan(咪哥杂谈)。
相关推荐
- Python开发管理神器--UV 使用教程:从安装到项目管理
-
UV是一个用Rust编写的高效Python包和项目管理工具,提供了比传统工具更快的速度和更强的功能。本文将指导你如何使用UV从安装到运行一个Python项目。重点:它可以独立安装,可...
- python入门-Day 26: 优化与调试(python优化方法)
-
优化与调试,内容包括处理模型运行中的常见问题(内存、依赖)、调整参数(如最大生成长度),以及练习改进Day25的文本生成结果。我会设计一个结构化的任务,帮助你掌握优化和调试技巧,同时提升模型性能...
- Python安装(python安装发生严重错误)
-
Windows系统1.安装python1.1下载Python安装包打开官方网站:https://www.python.org/downloads/点击"DownloadPython3.1...
- UV 上手指南:Python 项目环境/包管理新选择
-
如果你是一位Python开发者,曾因pipinstall的安装速度而感到沮丧,或者希望Python的依赖管理能够像Node.js那样高效顺滑,那么UV可能正是你所需要的工具。UV...
- uv——Python开发栈中的高效全能小工具
-
每天写Python代码的同学,肯定都离不开pip、virtualenv、Poetry等基础工具,但是对这些工具可能是又恨又离不开。那么有什么好的替代呢,虫虫今天就给大家介绍一个替代他们的小工具uv,一...
- 使用Refurb让你的Python代码更加优秀
-
还在担心你写的Python代码是否专业,是否符合规范吗?这里介绍一个Python代码优化库Refurb,使用它可以给你的代码提出更加专业的建议,让你的代码更加的可读,规范和专业。下面简单介绍这个库的使...
- 【ai】dify+python开发AI八字排盘插件
-
Dify插件是什么?你可以将Dify插件想象成赋予AI应用增强感知和执行能力的模块化组件。它们使得将外部服务、自定义功能以及专用工具以”即插即用”的简洁方式集成到基于Dify构建的AI...
- 零基础AI开发系列教程:Dify升级指南
-
Dify近期发布很是频繁,基本两三天一个版本。值得肯定的是优化和改进了很多问题,但是官方的升级文档有点分散,也有点乱。我这里整理了一个升级文档供大家参考,如果还没有升级到新版本的小伙伴,可以按照我的文...
- 升级到PyTorch 2.0的技巧总结(如何更新pytorch版本)
-
来源:DeepHubIMBA本文约6400字,建议阅读12分钟在本文将演示PyTorch2.0新功能的使用,以及介绍在使用它时可能遇到的一些问题。PyTorch2.0发布也有一段时间了,大家...
- dify 1.6.0版本发布解读:引入MCP支持与多项核心优化升级指南详解
-
2025年7月10日,dify发布了1.6.0版本。这是一次功能深度升级与性能优化的综合性更新,标志着dify在技术规范支持、操作体验以及系统稳定性方面迈出了重要的一步。本文将从核心新特性、功能增强、...
- Python教程(十四):列表(List)(python列表方法总结)
-
昨天,我们学习了变量作用域,理解了局部和全局变量的概念。今天,我们将开始探索Python的数据结构,从最常用的**列表(List)**开始。列表是Python中最灵活、最常用的数据结构,它可以存储不同...
- Python列表操作(python列表有哪些基本操作)
-
Python添加列表4分钟阅读在Python操作列表有各种方法。例如–简单地将一个列表的元素附加到for循环中另一个列表的尾部,或使用+/*运算符、列表推导、extend()和i...
- Python字符串变形术:replace替换+join连接,10分钟掌握核心操作
-
字符串替换魔法:replace()实战手册核心价值:一键更新文本内容,精准控制替换范围#基础替换:Python变Javas="hellopython"print(s.re...
- python集合set() 数据增册改查统计序循常用方法和数学计算
-
概念特点定义和创建常用操作集合间的关系集合数学操作集合生成式遍历概念:可变、无序、不重复的序列数据容器特点:无序,不支持下标唯一性,可以删除重复数据可修改定义和创建赋值法:语法:s={x,....
- Python列表方法append和extend的区别
-
在Python编程中,列表是一种非常常用的数据结构。而列表有两个方法append()和extend(),它们看起来有点相似,但实际上有着明显的区别。今天咱们就来好好唠唠这俩方法到底有啥不同。基本区别a...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python字典遍历 (54)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python读取文件夹下所有文件 (59)
- java调用python脚本 (56)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)