顾名思义,从字面意思可以理解为,它是用来"装饰"Python的工具,使得代码更具有Python简洁的风格。装饰器本质上是Python函数,能够实现让其他函数在不需要做任何代码变动的前提下增加额外功能。
为什么用装饰器?
装饰器是通过某种方式来增强函数的功能。当然,我们可以通过很多方式来增强函数的功能,只是装饰器有一个无可替代的优势——简洁且不改变函数内部代码。
只需要在被修饰函数上方填加一个@修饰函数,就可以对这个函数增加额外功能。
装饰器应用场景有哪些?
装饰器最大的优势是用于解决重复性的操作,其主要使用的场景有如下几个:
- 日志输出
- 类型检查
- 鉴权(权限认证)
- 重试等等
当然,如果遇到其他重复操作的场景也可以从装饰器的角度思考,如何提高代码的简洁性。通过可以抽离大量与函数功能本身无关的重复代码到装饰器中,概括的讲,装饰器的作用就是为已经存在的函数添加额外的功能。
简单示例
下面就以一个简单的例子来看一下它的作用。如果我们要对每个函数的增加重试机制,不使用装饰器,通常会是这样的,如下:
在 ops 方法里通过 for遍历以及try … except … else异常处理实现简单重试,执行上述代码,输出结果为:
接下来,我们使用装饰器,实现重试机制,具体实现如下:
通过编写一个重试机制的装饰器 fun_retry,被修饰函数ops的作为装饰器的参数,然后返回函数retry。
在 fun_retry中嵌套了一个 retry函数,那么retry是如何获取fun这个参数来执行的?,可是retry并没有接受fun这个传参,这就是Python里的闭包的概念,闭包就是指运行时自带上下文的函数,如这里的 retry函数,它运行的时候自带了上层函数 fun_retry传给他的fun这个函数,所以才可以在运行时对fun进行处理和输出。
然后再每个被修饰函数上面加上@fun_retry来调用装饰器,对不同的函数增加重试机制,即可省略每个函数里面的7行代码,实现在函数不需要做任何代码变动的前提下增加重试功能。
执行上述代码,输出结果为:
通过执行结果,也可以发现,当Python解释器执行到 @fun_retry时,就开始进行装饰了,相当于执行了如下代码:fun_retry(ops)。
回过头,我们深入的了解一下什么是闭包。
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
结合上面例子,一个闭包可以简单理解为调用了一个函数fun_a,这个函数fun_a返回了一个函数fun_b。这个返回的函数fun_b就叫做闭包。在调用函数fun_a的时候传递的参数a、c就是自由变量。
上面例子中,函数 fun_b 与环境变量 a,c 构成闭包。在创建闭包的时候,我们通过fun_a 的参数 a,c明确这两个环境变量的取值,因此确定了函数的最终形式(y = 2b + 10)。我们只需要变换参数a,b就可以获得不同的直线表达函数。由此,我们可以看到,闭包的引入提高代码了代码的可复用性,更加简洁。执行代码,输出结果如下:
带参数的装饰器
通过前面简单的实例介绍,应该已经大致清楚装饰器的作用和基本用法——通过闭包来实现装饰器,函数(ops)作为外层函数(fun_retry)的传入参数,然后在内层函数(retry)中运行、附加功能,随后把内层函数作为结果逐层返回。
除了上述简单的用法,装饰器还有更高的灵活性,例如带参数的装饰器。
带参数的修饰器并没有太复杂,其实就是在上述基本的装饰器的基础上在外面套一层接收参数的函数。
比如,我们认为现有的重试机制灵活性很差,需要它更加灵活的进行重试,例如支持修改重试次数、重试等待时间等,这时候可以把重试次数、重试等待时间作为装饰器的参数,如下:
可以看出,在原有的基础上装饰器外层又嵌套了一层函数fun_retry_more用来接收参数,这样的话在ops函数前面调用时可以给装饰器传入参数,这样的输出结果是:
可能有人会有一个疑问,如果我的被修饰函数 ops 需要参数怎么办?如果 ops 函数接收两个、三个参数,甚至更多呢?当装饰器不知道 ops 到底有多少个参数时,我们可以用*args 来代替,同样对于关键字参数,我们可以使用**kwargs来代替,args是一个数组,kwargs一个字典,我们在上面例子中也有所体现,如def retry(args, **kwargs)。这样它就能够接受任意数量和类型的参数并把它们传递给被包装的方法,使得我们能够用这个装饰器来装饰任何方法。
对带有返回值的函数进行装饰
我们仅需要对retry闭包进行如上修改,将fun()的返回值赋给re_v,并且retry闭包中增加return 返回re_v即可,执行上述代码,结果如下:
保留元信息的装饰器
在修饰器中有一个细节很少有人提及,那就是保留被修饰对象的元信息的装饰器
什么是函数的元信息?
就是函数本书的一些基本信息,例如函数名、函数文档等,我们可以通过func.__name__获取函数名、可以通过func.__doc__获取函数的文档信息,用户也可以通过注解等方式为函数添加元信息。
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、name、参数列表,如下:
使用装饰器极大地复用了代码,但是他有一个缺点,就是被修饰函数的元信息丢失了,执行上述代码,执行结果如下:
可以通过使用Python自带模块functools中的wraps来保留被修饰函数的元信息。
只需要在代码中加入@wraps(fun)即可保留函数的元信息。执行上述代码,输出结果为:
装饰器顺序
一个函数还可以同时定义多个装饰器,比如:
它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于: