在上一篇文章中,我们讲了使用装饰器overload函数来描述多态函数,那装饰器是什么呢?现在我们就来讲解一下装饰器的作用原理,以及我们自己该怎样制作装饰器。
定义
装饰器就是用来给一个函数增加额外功能的函数。就像我们的毛坯房也可以住人,但住起来不是很舒服,我们需要额外的给它装饰一番,让它更适合我们居住。装饰器只是在被装饰函数的外部增加一些我们需要的功能,它不能改变函数内部的逻辑。它就像回调函数的过程一样,把被装饰的函数传递给装饰器,装饰器在内部调用被装饰函数,并决定在什么条件下传什么值给被装饰函数。除了能控制传值外,如果被装饰函数有返回值时,还能得到被装饰函数的返回结果,可以进一步处理返回结果,最后返回执行这整套逻辑的函数。动态的装饰器,通常是一个闭包函数,它除了可以接收被装饰函数外,还能接收额外的参数,并根据传入参数的值来动态生成装饰函数,再返回这个装饰函数。记住装饰器一定要返回一个函数,返回的函数可以跟被装饰的函数有关,也可以跟被装饰的函数无关,总之一定要返回一个函数。因为一个函数被装饰器装饰后,我们再去执行这个函数时,其实执行的是装饰器返回的函数。如果装饰器返回的不是一个函数,那么将会抛出错误TypeError: xxx object is not callable,报错的意思就是装饰器返回的xxx对象不能被当作函数去执行。所以装饰器一定要返回一个函数(强调、强调、再强调)。
def 装饰函数(被装饰函数形参):
逻辑语句(可有可无)
return 函数
@操作符(运算符)
我们把被装饰函数传递给装饰器的方式,不是像回调函数一样把函数名当参数去传,而是使用@操作符来把被装饰函数传递给装饰器。这两种传参方式都可以传递函数,但使用@操作符更简单。@操作符主要使用在两个方面,一个是给装饰器传递被装饰函数,另一个是用来计算矩阵。关于使用@计算矩阵的知识点,本篇文章暂不介绍,这里主要使用它传递函数的功能。
@装饰函数
def 被装饰函数():
...
上面这种结构语句就是@操作符传递函数的方法,通过这种结构语句,@就能把被装饰函数传递给装饰函数,这都是python解释器内部定好的结构语句。所以@加装饰函数就构成了一个可以装饰其他函数的装饰器。
函数回调的方式传递函数
def description(func):
print(f'执行{func}函数')
func()
def hello():
print('hello world')
description(hello) # 按函数回调的方式执行
执行结果如下:
@操作符传递函数
def description(func):
print(f'执行{func}函数')
func()
@description
def hello():
print('hello world')
hello() # 直接执行
执行结果如下:
@操作符把hello函数传递给了description函数,@description就是一个装饰器。执行的方式跟函数回调完全不同,函数回调是把hello函数当作参数传入description函数,再执行description函数;@操作符是修饰了hello函数,执行的时候直接执行hello函数,就可以得到跟函数回调一样的结果唯一的区别是抛出了错误TypeError: 'NoneType' object is not callable,这就是因为装饰函数一定要返回一个函数,这里的description函数没有用return语句返回一个函数,所以python默认返回了None。至于为什么一定要返回一个函数,我们下面接着讲。
装饰器
下面我们来写一个最简单的装饰器。
def description(func):
"""
打印函数注释的装饰函数
:param func: 被装饰函数
:return: 被装饰函数
"""
help(func) # 使用python内置函数help打印出传入函数的注释信息
return func # 直接返回传入的函数
@description # 使用description装饰器装饰add函数,add函数会被@传递给description的形参func
def add(number_a: int, number_b: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:return: 求和结果
"""
return number_a + number_b
value = add(1, 2) # 执行add函数
print(f'计算结果为:{value}')
我们首先定义了一个装饰函数description,description可以接收一个函数,使用python内置函数打印出被装饰函数的注释信息,最后直接返回被装饰函数。现在我们使用description函数装饰了add函数,当我们执行add函数时,会首先执行装饰函数description,再把description返回的函数以add的名义来执行。首先执行description函数打印出add的标注信息,再执行返回的add函数,并把传入的值1和2都给返回的add函数,计算出求和结果3。执行结果如下:
我为什么要说执行的是返回的add函数,而不是定义的add函数。你以为执行的是add(1, 2),实际上执行的是func(1, 2)。因为传入的是add,所以func=add,这里给我们造成了一个错觉,执行add和执行func的结果是一样的。
下面我将再举一个例子,一个返回的函数不是传入函数的例子,来让大家更清晰的知道究竟执行的是那个函数。
def description(func):
"""
打印函数注释的装饰函数
:param func: 被装饰函数
:return: mul函数
"""
help(func)
def mul(a: int, b: int): # 定义一个闭包函数
"""
求积
:param a: 乘数a
:param b: 乘数b
:return: 求积结果
"""
return a * b
return mul # 不返回func而返回mul
@description # 使用description装饰器装饰add函数
def add(number_a: int, number_b: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:return: 求和结果
"""
return number_a + number_b
value = add(3, 6) # 执行add函数
print(f'计算结果为:{value}')
我们在装饰函数description内定义了一个mul函数,mul是一个用来求积的乘法函数。现在我们不再返回func而是返回mul,再来看一下执行结果。
看到了没,计算结果是18,说明执行的是乘法。如果执行的是加法的话,计算结果应该是9。所以add(3, 6)执行的其实是description返回的mul函数,再把传入的值给了mul,mul(3, 6)得到结果18。我们虽然把add传给了description,但是除了在description中打印出add的注释以外,并没有使用add的内部逻辑,如果我们想要使用add的逻辑,就必须返回func。
现在我们应该明白装饰函数一定要返回一个函数了吧,因为一个函数被装饰以后,我们再执行这个函数时,其实执行的不是这个函数,而是先执行装饰函数中的逻辑,然后再执行装饰函数返回的函数。并且有传入值时,要把传入的值传给返回的函数去执行。还是不能理解的话,请参考下图的逻辑关系去理解。
现在我们可以得到一个结论: add = description(add) ,所以 description(add) 必须返回一个函数,不然add()没法执行。
当add函数被description函数装饰以后,add函数就变成了description(add)。我们使用print(add)打印add的函数地址也会得到同样的结果:
add没有被装饰之前红色划线部分是add,被description装饰后变成了description.<locals>.mul。其中locals表示的是我们定义的add函数原本的逻辑,所以虽然add指向的函数逻辑已经变成了description(add),但我们定义的add逻辑还是被原本的保留在了description函数的逻辑中,只不过这时add的原本逻辑被赋值给了形参func。我们在description中执行func就是执行原本的add逻辑,返回func就是返回原本的add逻辑。
进阶
现在我们知道了装饰器的基本原理,那么我们就来试着制作一些有用的装饰器吧。我们在装饰函数内部执行被装饰函数,并进一步处理结果。
def description(func):
"""
把函数的结果乘以3的装饰函数
:param func: 被装饰函数
:return: closure
"""
def closure(a, b):
print(f'开始执行{func.__name__}函数') # 使用__name__得到func函数变量名
result = func(a, b)
print(f'{func.__name__}({a}, {b})的计算结果为: {result}')
return result * 3
return closure
@description # 使用description装饰器装饰add函数
def add(number_a: int, number_b: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:return: 求和结果
"""
return number_a + number_b
value = add(3, 6) # 执行add函数
print(f'{add.__name__}的计算结果为: {value}')
description函数返回的函数是closure函数,所以value = add(3, 6)就相当于closure(3, 6)。closure函数是一个闭包函数,closure中执行了func函数,并把自己接收到的值传给func进行计算。此刻func的计算逻辑就是我们定义的add,所以func(3, 6)得到结果9,closure函数返回27。执行结果如下:
我们为了减少代码的行数,通常会写一个装饰器来装饰多个函数,把这些函数共有的运算逻辑都写到装饰器中,被装饰的函数中只留下核心运算逻辑。但是在装饰多个函数时,不是所有的被装饰函数形参数量都一样多,可能有的接收3个参数、有的接收4个参数、有的不接收参数。那我们该怎么办呢,这时我们就需要使用*args和**kwargs来做形参了。关于*args和**kwargs的使用方式在上一篇文章python定义函数中已经介绍过了,这里就不再赘述了。
下面来给大家展示一个装饰器装饰多个函数的例子。
def description(func):
"""
用于捕获函数运行错误的装饰函数
:param func: 被装饰函数
:return: closure
"""
def closure(*args, **kwargs):
print(f'开始执行{func.__name__}函数')
try:
return func(*args, **kwargs)
except Exception as e:
print(f'执行{func.__name__}函数出错,报错信息如下: ')
print(e)
return closure
@description # 使用description装饰器装饰add函数
def add(number_a: int, number_b: int, number_c: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:param number_c: 整数c
:return: 求和结果
"""
return number_a + number_b + number_c
@description
def div(number_a: int, number_b: int):
"""
求商
:param number_a: 整数a
:param number_b: 整数b
:return: 求商结果
"""
return number_a / number_b
value = add(1, 2, 3)
print(f'{add.__name__}的计算结果为: {value}')
value = div(3, 0)
print(f'{div.__name__}的计算结果为: {value}')
我们经常使用try语句来给函数捕获错误,如果我们的代码中所有函数的错误捕获方式都是一样的,我们就完全可以使用装饰器来减少代码。执行结果如下:
从计算结果中我们可以看到装饰器成功捕获到了函数的错误,但是closure的计算结果为: 输出了两次,这样看起来不太舒服。我们能不能直接输出add的计算结果为: 6和div的计算结果为: None,这样看起来就舒服多了。就相当于把func的所有描述信息都给到closure,functools库中的wraps装饰函数恰好可以做到。
@wraps(func)
wraps装饰函数可以把函数A的所有描述信息给到函数B,替换掉函数B原本的信息,但是不影响函数B的运算逻辑。用法如下:
from functools import wraps # 从functools中导入wraps装饰函数
def hello():
"""打印hello world"""
print('hello world')
@wraps(hello) # 把hello的所有描述信息给test
def test(*args):
"""测试函数"""
print(args)
print(test)
help(test)
test(1, 2, 3) # (1, 2, 3)
执行结果如下:
看到没test函数的一切描述信息都变成了hello的描述信息了,函数名变了、函数注释也变了,但是test函数的运算逻辑却没有变化。这就是wraps的作用。那么现在我们把wraps加入到装饰函数中,再来看一看执行的结果吧。
from functools import wraps
def description(func):
"""
用于捕获函数运行错误的装饰函数
:param func: 被装饰函数
:return: closure
"""
@wraps(func) # 使用wraps把函数func的描述信息给closure函数
def closure(*args, **kwargs):
print(f'开始执行{func.__name__}函数')
try:
return func(*args, **kwargs)
except Exception as e:
print(f'执行{func.__name__}函数出错,报错信息如下: ')
print(e)
return closure
@description # 使用description装饰器装饰add函数
def add(number_a: int, number_b: int, number_c: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:param number_c: 整数c
:return: 求和结果
"""
return number_a + number_b + number_c
@description
def div(number_a: int, number_b: int):
"""
求商
:param number_a: 整数a
:param number_b: 整数b
:return: 求商结果
"""
return number_a / number_b
value = add(1, 2, 3)
print(f'{add.__name__}的计算结果为: {value}')
value = div(3, 0)
print(f'{div.__name__}的计算结果为: {value}')
执行结果如下:
现在执行结果看起来就好多了,使用wraps的装饰函数在函数执行日志和函数错误抛出方面都非常好用。wraps让装饰函数返回的函数与被装饰函数拥有了相同的身份,在unittest和pytest框架中输出测试报告方面有出色的表现。
现在我们应该会使用装饰器了吧,那下面我们将再进一步,学习可以传参的装饰器。
动态装饰器
当我们想在装饰函数中加入一些动态变化时,我们就需要把装饰函数变成一个闭包函数。通过在装饰函数的外面再加一层函数,我们给外层函数传入不同的值,装饰函数就执行不同的逻辑。例如我们想控制装饰函数对被装饰函数结果的处理逻辑,我们可以进行如下操作:
from functools import wraps
def times(number: int):
"""
把局部变量number给内层函数
:param number: 倍数
:return: description
"""
def description(func):
"""
把函数的结果乘以number的装饰函数
:param func: 被装饰函数
:return: closure
"""
@wraps(func)
def closure(*args, **kwargs):
print(f'开始执行{func.__name__}函数')
try:
result = func(*args, **kwargs)
return result * number # 将函数的结果乘以number再返回
except Exception as e:
print(f'执行{func.__name__}函数出错,报错信息如下: ')
print(e)
return closure
return description
@times(3) # 使用description装饰器装饰add函数
def add(number_a: int, number_b: int):
"""
求和
:param number_a: 整数a
:param number_b: 整数b
:return: 求和结果
"""
return number_a + number_b
value = add(2, 3)
print(f'{add.__name__}的计算结果为: {value}')
看似在用times(3)来装饰add函数,但是times(3)会返回description函数。所以还是在用description装饰add函数,而且times(3)把3赋值给了局部变量number。times函数执行完毕释放资源的时候会把局部变量number给description函数,这是闭包函数的特性。如此一来,我们就可以控制description函数中number的值了,也就可以控制返回值的倍数了。times(3)就是返回3倍add的结果,我们也可以设置成其他倍数,执行结果如下:
这就是动态装饰函数的写法,现在我们就大胆的根据自己的思路去写出自己想要的装饰器吧。
多重装饰结构
@a_description # 第三层装饰 a_description(b_description(c_description(function)))
@b_description # 第二层装饰 b_description(c_description(function))
@c_description # 第一层装饰 c_description(function)
def function() # 被装饰函数
...
有时候我们想给一个函数添加多个额外的功能,这时只用一个装饰器可能已经不能达到我们的需求了。那么现在我们有两种选择,一种是直接修改这个装饰器,让这个装饰器拥有更多的功能来满足我们的需求。但是如果这个装饰器还装饰了其他函数,而其他函数又不需要这么多额外功能,我们就不能直接修改这个装饰器了。另一种是使用多个装饰器来装饰函数,让函数额外拥有多个功能,就像俄罗斯套娃一样在函数外套上一个又一个的装饰器。
from functools import wraps
def print_annotation(func):
"""
打印函数注释
:param func: 被装饰函数
:return:
"""
help(func)
return func
def catch_error(func):
"""
捕获错误
:param func: 被装饰函数
:return:
"""
@wraps(func)
def closure(*args, **kwargs):
print(f'catch_error: 准备捕获{func.__name__}函数执行错误')
try:
result = func(*args, **kwargs)
print(f'catch_error: 运算结果为{result}')
return result
except Exception as e:
print(f'执行{func.__name__}函数出错,报错信息如下: ')
print(e)
return closure
def times(number):
"""
把局部变量number给内层函数
:param number: 倍数
:return: description
"""
def description(func):
"""
把函数的结果乘以number的装饰函数
:param func: 被装饰函数
:return: closure
"""
@wraps(func)
def closure(*args, **kwargs):
print(f'description: 给{func.__name__}的运算结果乘以{number}')
result = func(*args, **kwargs) * number
print(f'description: 运算结果为{result}')
return result
return closure
return description
@print_annotation # 使用print_annotation装饰catch_error(description(add))函数
@catch_error # 使用catch_error装饰description(add)函数
@times(5) # 使用description装饰add函数
def add(a: int, b: int):
"""
求和
:param a: 整数a
:param b: 整数b
:return: 求和结果
"""
result = a + b
print(f'add: 运算结果为{result}')
return result
value = add(2, 3)
print(f'运算结果为{value}')
执行结果如下:
从执行结果中我们可以看到多重装饰结构的执行过程类似函数递归的过程,add函数在经过3个装饰函数的修饰后变成了print_annotation(catch_error(description(add)))函数。在执行的时候最先执行print_annotation函数,打印出了add的注释,返回了catch_error(description(add))函数;接着执行catch_error(description(add))函数函数,打印出了准备捕获add函数的执行错误;然后执行description(add)函数,打印出了给add的运算结果乘以5;最后执行add函数,返回2+3的结果5;description(add)把add的返回值5乘以5,返回运算结果25;catch_error(description(add))把description(add)的返回值25直接返回出来。我们就得到了最终的运算结果25,思维方式参考下图: