在加班赶工的时候, 最让人害怕遇到的情况, 就是需要去修改一大堆重复代码的这种非常令人头疼的事情。
今天, 我给你提供一条最短的路径。通过这条路径, 你可以在三分钟之内完全弄明白装饰器这个技术概念。
我们先从结论开始, 再学习具体步骤。
我们先说结论, 装饰器这个事物, 它的本质, 实际上就是一个像套娃一样嵌套的函数。它能够实现这样一种效果, 就是在完全不修改原函数的原有代码的情况之下, 给原函数去额外地增加一些新的功能。
具体操作起来非常的简单, 你只需要在原函数的上面添加一种特定类型的符号, 就能够让该函数在瞬间具备全新的功能技能。
一、什么是装饰器?一杯奶茶秒懂
假设你开办了一间专门售卖奶茶的店铺, 此前店内只供应原味的奶味饮品。目前你打算在提供的所有品种之中增加珍珠作为配料元素。但是你同时又希望能够避免去重新编写或者修订制作该饮品的具体工作步骤或操作流程。
最简略的操作途径在于雇佣一名专门的包装操作员工, 在接收到顾客发出的点单指令之后, 这名工作人员首先是拿出那杯原本没有添加任何配料的原味奶茶, 紧接着再向这杯奶茶的内部空间里面加入珍珠这种小颗粒食材, 最终完成整个流程并将它递交到购买者的手中。
在这其中, 这个叫做包装员的东西, 实际上就是那个装饰器, 而原味奶茶, 就是你亲手写出来的普通的那一个函数, 至于说要加上珍珠, 那其实就是新增的功能部分了, 这个装饰器会把原来的那个函数给包裹起来这么做做的结果是不会去改变原来函数内部的逻辑的与此同时呢它还扩展了原来的那个功能。
二、底层逻辑与基础写法
我们来看一下那些最基础的代码结构, 定义一个装饰器函数, 这个装饰器函数会接收一个函数, 作为它的参数。
在装饰器内部去定义一个包装函数。在这个包装函数的里面, 首先去执行那些需要新增的功能, 接着去调用原来的那个函数, 最后再去执行一些用来收尾的功能。然后, 让那个装饰器函数把这个包装函数给返回出去。
在进行使用操作的时候, 你得在那个目标函数的上面位置, 去书写那个装饰器的名字, 并且还要加上一个@符号。当你去调用这个目标函数的时候, 实际上你所调用的其实是那个被包装之后的函数。
我们可以给出一个最为浅显易懂的实例, 那就是为函数添加上关于执行时间的统计功能。
你可以尝试编写一个具备计时功能的装饰器代码。在实现这个包装函数的具体流程时, 首先需要先记录初始的开始时间点, 随后去执行原本需要被调用的函数逻辑操作, 最后再获取当前的结束时间点。通过这两个时间数据的对比与计算, 能够得出整体任务所消耗的总时长, 并将该耗时信息直接打印展示出来供查看。
于是乎, 对于任何涉及到需要测定速度方面的函数来说, 只要给它们加上这一个特定的符号标记就行了, 这样便能自动地打印出运行的时间状况, 从而做到完全不需要对其内部的代码进行任何修改操作。
三、用户想知道应该如何去书写带有参数的装饰器。
在实际的开发流程当中, 那些本来就已经存在的原始函数, 通常是会带着相应的参数存在的。面对这一种状况, 在装饰器内部所设计出来的那个包装函数的角色, 就必须得要具备能够接收任意多种类数量参数的能力才行。
在函数定义里面, 通过使用加上了星号的 args 来接收那些位置参数, 同时使用加上了两个星号的 来接收那些关键字参数。
这样做的话, 就把这些已经接收到的参数, 按照它们原来的样子, 原封不动地传递给了最初的那个函数, 从而就能保证这个原始函数在接收参数和传递参数的时候, 绝对不会出现什么差错误或失误的情况。
另外, 要是那个装饰器自己也得要有参数呢, 举例说来说去, 就是要指定一下日志的级别之类的这种情况, 那样子话, 就必须还要再多弄一层函数出来才行。
在最外层的位置接收装饰器所提供的参数, 而在中间层的位置则是接收那个原始的函数, 至于最内层的结构, 它是一个被包装好的函数。尽管这种安排看起来就像是一个又一层层的套娃模型, 但是其内在的逻辑关系依然保持着清晰的条理。
具体而言, 外层的函数会将中间层的函数返回出去, 紧接着中间层的函数又会将最内层的包装函数返回出来, 整体呈现出一种一步接着一步层层递进的状态。
四、三大高频实战场景
在第一个场景当中, 用户进行登录校验。
在很多情况下, 后台端的多个接口功能是必须要求用户处于登录状态的。如果我们不采用装饰器的形式来处理这一需求, 那么对于每一个相关的接口来说, 都需要单独编写一遍用于检查用户登录权限的代码。
有了装饰器之后, 可以写一个用来做登录校验的装饰器。在那个被包装起来的函数里面检查Token。如果用户没有登录, 就直接返回错误信息。如果用户已经登录了, 然后再去执行原本的那个接口逻辑。所有那些需要校验的接口, 只要加上一个符号就能够搞定了。这样的代码复用率非常之高。
场景二:接口重试机制。
网络请求会经常性地由于波动状况而导致失败现象。因此建议编写一种针对重试的功能装饰器单元, 该单元支持接收用以界定最大允许重试操作次数作为参数选项设定。
在包装函数的内部, 使用循环的方式去调用原始函数,如果遇到错误, 并且重试的次数还没有用完, 就休眠一秒之后重新进行尝试, 只要取得成功就直接将结果返回, 这样能够让代码的健壮性得到大幅度的提升, 完全不用再编写一大堆非常难看的异常捕获代码。
场景三是这样的, 我们需要把那些计算好的结果给缓存起来。
有些函数计算起来会耗费特别长的时间, 举例来说就是那些复杂的数学运算。请你去写一个缓存装饰器, 利用字典这个工具把输入的参数还有输出的结果都存进去。
在启动调用动作之前, 先去查字典里面是否存有了对应的内容, 要是找到了就马上给予返回, 要是没有找到就去算个结果并且往字典里面放好, 这个办法能够省去好多来回做的重复计算的功夫, 让写好的程序能够跑得非常迅速。
五、新手避坑指南
第一个坑, 原函数的元信息出现了丢失的情况。
在使用装饰器以后, 原函数所拥有的名字和注释就会变为包装函数的信息了。其解决办法是, 在包装函数的上方加上库中所含有的 wraps 装饰器, 从而将原函数的元信息复制过来。这一做法在处理编写框架以及大型项目时属于至关重要的环节。
这里存在着第二个问题, 那就是在类的内部方法里使用了装饰器功能。
要是把装饰器用在类的实例方法上面, 那个被包装起来的函数, 它的第一个参数必须得是 self 这个玩意儿才行, 不然的话, 系统就会把 self 当作是一个普通的参数直接传给原来的函数, 这就会导致程序报错, 出问题, 所以大家一定要确保那个参数的传递顺序跟原来方法的参数顺序是一模一样的, 完全不能差一点。
第三处存在的不足之处是关于装饰器具体在执行环节上的时机问题。
当你使用装饰器的时候, 它在代码被加载的那个瞬间就会被执行一遍, 这个动作并不是等到你去调用函数的时候才发生的。因此, 如果你的装饰器内部包含了一些非常耗时的操作, 举例来说去读取一个很大的文件, 那么这种做法就会直接导致程序的启动速度变得缓慢。
所以大家在写代码的时候应该尽量把那些比较耗费时间的操作放在包装函数的内部区间里, 绝对不要把它堆砌在装饰器的外部区域中。
六、我们来对核心的重要内容进行一个整理和归纳。
装饰这个东西其实是用来把代码的复用率给提上去的好工具。它的核心想法就是让一个函数套在另一个函数里面, 然后靠着闭包这个机制来把原函数的引用给保留下来。
你要是能把那个带参数传递的操作给搞明白, 同时还知道怎么用wraps这个东西去保留元信息的话, 那就基本上能够解决绝大多数的在日常工作中的需要了。
建议你可以多动手写上几个用于计时器方面的装饰器, 也再写几个用于日志记录方面的装饰器来练习练习, 当你这么做了以后, 你就会突然感觉到, 自己写的代码会变得清爽非常多的。
大家觉得里面还有哪些语法特性跟装饰器一样, 是让人家又爱又恨的? 欢迎你在评论区里面留言, 大家一起讨论, 也别忘了点赞和关注, 我们每天带你看一点点关于编程的实用干货。