python-闭包和装饰器-02-装饰器(decorator)
装饰器(decorator)
理解了上一章的闭包之后,装饰器就是闭包的一种应用,只是外部函数的参数传入的不是普通的变量类型,而是传入一个函数名。装饰器一般用于:不修改被装饰函数(即外部函数传入的参数)内部代码的情况下,对对装饰函数功能的新增或者拓展,比如,想知道某一个函数总共运行了多长时间,可以加一个装饰器,记录该函数在被调用前后的当前时间,再相减得到程序的运行时间,再比如在调用某个程序前后打印一些日志信息,再比如在调用某个程序前增加一些权限验证或者数据验证等。当然这些功能,直接写在函数调用的前后也可以实现,但是如果有n个函数都需要这个功能,那么就需要写上多遍,维护起来比较麻烦。这里给出一个简单的装饰器的例子,在调用函数test1时在原来的打印功能前新增一个权限验证的功能:
def set_func(func): def cal_func(): print('--------新增权限验证功能-------') func() return cal_func @set_func def test1(): print('-------调用了test1--------') test1()
# 运行结果为:
--------新增权限验证功能-------
-------调用了test1--------
装饰器的手动实现过程(装饰器原理)
def set_func(func): def cal_func(): print('--------进行数据验证-------') print('此时变量func指向:', func) func() return cal_func def test1(): print('-------调用了test1--------') print('装饰前,test1指向:', test1) print('1.新建变量ret接收set_func的返回值') ret = set_func(test1) print('2.此时ret指向:', ret) print('3.调用ret') ret() # 运行结果为: 装饰前,test1指向: <function test1 at 0x000001F2220BA1F0> 1.新建变量ret接收set_func的返回值 2.此时ret指向: <function set_func.<locals>.cal_func at 0x000001F2220BA280> 3.调用ret --------进行数据验证------- 此时变量func指向: <function test1 at 0x000001F2220BA1F0> -------调用了test1--------
1、定义了一个闭包,外部函数为set_func,传入参数为func,内部函数为cal_func,外部函数返回内部函数
2、定义了一个普通函数test1,函数名即变量名,此时变量test1指向名为test1的函数的地址....BA1F0
3、调用set_func:
3.1.将上面的test1传入,此时set_func的形参func,指向了实参test1的指向,即....BA1F0
3.2.定义了一个名为cal_func的函数,并创建其对应的内存地址,并将其指向(或引用)返回给变量ret,即ret指向了名为cal_func的函数的地址BA280
4、调用ret函数,即执行名为cal_func内部中的代码:
4.1.打印数据验证,打印func的指向
4.2.调用func(),因为func指向的是名为test1的函数的地址,因此执行test1中的代码,打印‘-------调用了test1--------’
至此运行完毕,成功在打印调用test1之前打印出了数据验证的代码。但是本来我们调用的是test1,现在为了加上数据验证功能,需要让改成调用ret,这样还是修改了原来的代码。接下来将程序进一步修改,我们之前使用的变量名为ret的变量接收set_func的返回值,我们也可以把ret换成aaa、bbb、foo都可以,现在我们把变量名test1,在上个代码版本上添加 ‘===’分割线 后面的代码
def set_func(func): def cal_func(): print('--------进行数据验证-------') print('此时变量func指向:', func) func() return cal_func def test1(): print('-------调用了test1--------') print('装饰前,test1指向:', test1) print('手动实现装饰器:') print('1.新建变量ret接收set_func的返回值') ret = set_func(test1) print('2.此时ret指向:', ret) print('3.调用ret') ret() print('=' * 30, '我是分割线', '=' * 30) print('1.将上述ret变量名换成test1') test1 = set_func(test1) print('2.此时test1指向:', test1) print('3.调用test') test1()
运行结果为:
装饰前,test1指向: <function test1 at 0x0000019042CA0280> 手动实现装饰器: 1.新建变量ret接收set_func的返回值 2.此时ret指向: <function set_func.<locals>.cal_func at 0x0000019042CA0310> 3.调用ret --------进行数据验证------- 此时变量func指向: <function test1 at 0x0000019042CA0280> -------调用了test1-------- ============================== 我是分割线 ============================== 1.将上述ret变量名换成test1 2.此时test1指向: <function set_func.<locals>.cal_func at 0x0000019042CA03A0> 3.调用test --------进行数据验证------- 此时变量func指向: <function test1 at 0x0000019042CA0280> -------调用了test1--------
1、在调用set_func函数时,返回cal_func的地址引用给变量test1,即在调用前,原来的变量test1指向的是函数名为test1的地址...CA0280(注意这里程序每次运行时,给同一个函数名创建的内存地址可能会存在不一样,所以这里地址和上面程序的输出结果打印的地址不一样),现在调用后,将变量的test1的指向变成了指向cal_func的地址....CA03A0。而每次调用set_func时都会让形参func指向函数test1的地址...CA0280,因此虽然变量test1不再指向原函数test1的地址,但是一定会有一个形参指向函数test1的地址并保存了起来,这就是前面说的闭包,调用闭包外部函数时,会将传入的参数进行保存,并在调用内部函数时使用到保存的外部参数。
2、set_func返回函数cal_func的引用给变量test1后,再次调用test1(),即实际调用的是cal_func方法,在cal_fun方法中,打印数据验证,并调用func(),func指向的是函数test1,即调用函数test1,打印test中的内容
上述将变量test1由之前指向函数test1,改变为指向set_func的返回值即cal_func的过程,对应代码为test1 = set_func(test1),即为装饰的过程。为了简写,而不需要每次都写上test1 = set_func(test1),就可以在函数test1定义前加上@set_func,即@set_func等价于test1 = set_func(test1)
完善装饰器
1.给装饰器内部方法添加参数和返回值
因为一个装饰器可能对多个函数进行装饰,而这多个函数可能参数不一样,或者是否有返回值也不一样,为了使装饰器适配所有的被装饰函数,需要给装饰器添加参数和返回值,如:
# 装饰器为了满足被装饰函数的所有参数和返回值 # 1.需要在cal_func中写上不定长参数*args和**kwargs # 1.1 注意使用的参数名就叫做args和kwargs,而不是*args和**kwargs # 这里参数中写的*和**是为了让python解释器将多余的参数分为元组tuple和字典dic # 1.2 而下面调用func传入的参数时,也加上了*和**,这里*和**的作用是将args和kwargs拆包 # 使传入的参数拆成在同一个维度上面,相当于拆成func('a', 'b', 'c', d=1, e='2') # 如果调用func传入时参数不带*,则相当于传入的参数为func(('a', 'b', 'c'), {'d': 1, 'e': '2'}) # 这样会导致解释器解析func的参数时,认为传入的是两个值,一个是元组,一个是字典 # 然后在func中处理参数时,将第一个元组通过para接收,第二个字典通过*args接收,导致最终结果与预期不一样 # 2.在cal_func中调用传入的func前,添加return,这样如果被装饰函数也有return的话,可以将最终的结果也return出来,如test2 def set_func(func): def cal_func(*args, **kwargs): print('------数据验证------') print('args', args) print('kwargs', kwargs) return func(args, kwargs) return cal_func @set_func def test1(para, *args, **kwargs): print('------调用test1------') print('------para------', para) print('------*args------', args) print('------*kwargs------', kwargs) @set_func def test2(para, *args, **kwargs): print('------调用test2------') print('------para------', para) print('------*args------', args) print('------*kwargs------', kwargs) return 'ok...' test1(1) print('=' * 30, '我是分割线', '=' * 30) test1('a') print('=' * 30, '我是分割线', '=' * 30) test1('a', 'b', 'c') print('=' * 30, '我是分割线', '=' * 30) test1('a', 'b', 'c', d=1, e='2') print('=' * 30, '我是分割线', '=' * 30) print(test2('a', 'b', 'c', d=1, e='2'))
运行结果为:
------数据验证------ args (1,) kwargs {} ------调用test1------ ------para------ (1,) ------*args------ ({},) ------*kwargs------ {} ============================== 我是分割线 ============================== ------数据验证------ args ('a',) kwargs {} ------调用test1------ ------para------ ('a',) ------*args------ ({},) ------*kwargs------ {} ============================== 我是分割线 ============================== ------数据验证------ args ('a', 'b', 'c') kwargs {} ------调用test1------ ------para------ ('a', 'b', 'c') ------*args------ ({},) ------*kwargs------ {} ============================== 我是分割线 ============================== ------数据验证------ args ('a', 'b', 'c') kwargs {'d': 1, 'e': '2'} ------调用test1------ ------para------ ('a', 'b', 'c') ------*args------ ({'d': 1, 'e': '2'},) ------*kwargs------ {} ============================== 我是分割线 ============================== ------数据验证------ args ('a', 'b', 'c') kwargs {'d': 1, 'e': '2'} ------调用test2------ ------para------ ('a', 'b', 'c') ------*args------ ({'d': 1, 'e': '2'},) ------*kwargs------ {} ok...
2.给装饰器添加参数
目前的装饰器在装饰不同的函数时,前面的装饰的内容都是一致的,那么在同一个装饰器中能不能给不同的被装饰函数添加不同的功能呢?比如在装饰test1时,添加test1的数据验证,装饰test2时,添加test2的装饰验证。
目前装饰器外层函数的参数是固定的,用来接收被装饰的函数,内层函数的参数也是固定的,用来接收传入被装饰函数的参数。那么就需要在现有装饰器外再套一层parent_func函数,其中设置一个参数用来接收是哪一个被装饰函数的名字,即:
def parent_func(name): def set_func(func): def cal_func(*args, **kwargs): print('------%s的数据验证------' % name) return func(args, kwargs) return cal_func return set_func @parent_func('test1') def test1(para, *args, **kwargs): print('------调用test1------') @parent_func('test2') def test2(para, *args, **kwargs): print('------调用test2------') return 'ok...' test1(1) print('=' * 30, '我是分割线', '=' * 30) print(test2('a', 'b', 'c', d=1, e='2')) # 运行结果为: ------test1的数据验证------ ------调用test1------ ============================== 我是分割线 ============================== ------test2的数据验证------ ------调用test2------ ok...
1、定义再添加一层函数parent_fun,接收一个或多个参数,看具体业务需求,该parent_fun函数返回原来的装饰器函数
2、将原来的@set_func改成@parent_func(xxx),可以理解为当运行到parent_func('test1')这行代码时,即会调用parent_func函数,返回原来的那个装饰器,再让原来的装饰器对函数进行装饰
多个装饰器对同一个函数进行装饰
有时候我们需要对一个函数添加好几个装饰器呢,如一个是权限验证的装饰器,一个是数据验证装饰器等,如:
def set_func1(func): print('----1.权限验证开始装饰----') def cal_func(*args, **kwargs): print('------1.权限验证------') return func(args, kwargs) return cal_func def set_func2(func): print('----2.数据验证开始装饰----') def cal_func(*args, **kwargs): print('------2.数据验证------') return func(args, kwargs) return cal_func @set_func1 @set_func2 def test1(para, *args, **kwargs): print('------调用test1------') print('开始调用test1') test1(1)
运行结果:
----2.数据验证开始装饰----
----1.权限验证开始装饰----
开始调用test1
------1.权限验证------
------2.数据验证------
------调用test1------
1、创建了两个装饰器,set_func1和set_func2,对test1装饰时,@set_func1写在@set_func2前面
2、根据打印结果可以发现两个现象:
2.1.'开始装饰'的打印在'开始调用'之前,说明了装饰这个过程并不是在调用被装饰函数才进行装饰,而是在运行到@xxxxx时就开始装饰了
2.2.在装饰的时候,是从下往上顺序装饰的,而调用时,是从上往下调用的装饰器,可以这么理解:解释器从上往下执行,当执行到第一个装饰器@set_func1时,发现下面一行代码并不是定义一个函数,则跳过改行代码,继续往下执行,执行到@set_func2,发现set_func2的下一行是定义一个函数,那么就开始set_func2的装饰,装饰完之后,再回到上一行开始set_func1对刚才装饰的结果再进行一层装饰,这样的结果就是最外层是set_func1,中间层是set_func2,最里层是test1。那么最后在调用test1的时候,从外往里执行,就先执行的set_func1再执行set_func2,最后执行test1
装饰器会改变原函数变量的一些函数属性
使用了装饰器后,由于指向原函数的变量指向了装饰器内部定义的函数,因此如果调用原函数变量的__name__和__doc__等方法时,得到的是装饰器内部函数的值,如:
def set_func(func): def cal_func(): """this is cal_func doc""" print('cal_func')
func() return cal_func @set_func def test(): """this is test doc""" print('test') print(test.__name__) print(test.__doc__) # 运行结果 # cal_func # this is cal_func doc
可以看出打印出来的是内部函数cal_func的属性,即写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。因此为了解决这个问题,任何时候定义装饰器的时候,都应该使用 functools
库中的 @wraps
装饰器来装饰内部函数。例如:
from functools import wraps def set_func(func): @wraps(func) def cal_func(): """this is cal_func doc""" print('cal_func')
func() return cal_func @set_func def test(): """this is test doc""" print('test') print(test.__name__) print(test.__doc__) # 运行结果 # test # this is test doc
在类中定义装饰器并调用
from functools import wraps class A: def logger1(func): """简单装饰器(不带参数)""" @wraps(func) def wrapper(self, *args, **kwargs): print('logger1 start') func(self, *args, **kwargs) print('logger1 end') return wrapper def logger2(log_data): """复杂装饰器(带参数)""" def inner(func): @wraps(func) def wrapper(self, *args, **kwargs): print('logger2 start') print(log_data) func(self, *args, **kwargs) print('logger2 end') return wrapper return inner @logger1 def f1(self): print('f1') @logger2('这是参数') def f2(self): print('f2') # 测试结果 a = A() a.f1() print('-----------------------') a.f2()
返回结果为:
logger1 start f1 logger1 end ----------------------- logger2 start 这是参数 f2 logger2 end