Python学习之路day4-函数高级特性、装饰器
一、预备知识
学习装饰器需理解以下预备知识:
函数即变量
函数本质上也是一种变量,函数名即变量名,函数体就变量对应的值;函数体可以作为值赋给其他变量(函数),也可以通过函数名来直接调用函数。调用符号即()。
嵌套函数
函数内部可以嵌套定义一层或多层函数,被嵌套的内部函数可以在外层函数体内部调用,也可以作为返回值直接返回
闭包
在一个嵌套函数中,内部被嵌套的函数可以调用外部函数非全局变量并且不受外部函数声明周期的影响(即可以把外部函数非全局变量视为全局变量直接调用)。
高阶函数
把一个函数作为参数传递给另外一个函数,并且把函数名作为返回值以便调用函数。
Python中变量赋值及调用机制
python中变量定义的过程如下:
1. 在内存中分配一块内存空间;
2. 将变量的值存放到这块内存空间;
3. 将这块内存空间的地址(门牌号)赋值给变量名(即变量名保存的是内存空间地址)
总结:变量保存的不是变量对应的真实值,而是真实值所被存放的内存空间地址,这也就意味着变量的调用需要通过调用内存空间地址(门牌号)来实现。将变量延伸到函数,函数和函数的参数都属于变量,调用函数进行参数传递时,是对函数和参数两个变量的同时调用,符合变量赋值及调用的机制(间接引用而非直接调用变量对应的值)。参数的传递实质上是一种引用传递,即通过传递对实参所在内存空间地址的指向来完成传递过程。
这一机制可通过id()来验证:
1 list1 = ['Python', 'PHP', 'JAVA'] 2 list2 = list1 3 print(id(list1), '========', id(list2)) 4 print('') 5 6 7 def foo1(x): 8 print(id(foo1)) 9 print(id(x)) 10 print(foo1) 11 12 13 def foo2(): 14 pass 15 16 foo1(foo2()) 17 print('---------') 18 print(id(foo1)) 19 print(id(foo2())) 20 print(foo1) 21 22 输出: 23 7087176 ======== 7087176 #普通变量调用,内存地址指向相同 24 25 7082192 26 1348178992 27 <function foo1 at 0x00000000006C10D0> 28 --------- # 函数调用前后,不仅函数名指向的内存地址相同,实参和形参的内存地址也相同 29 7082192 30 1348178992 31 <function foo1 at 0x00000000006C10D0>
二、装饰器的需求背景
设想这样一个现实场景:自己开发的应用在线上稳定运行了一年,后面随着业务的发展发现原有的通过某些函数定义的部分功能需要扩展一下新功能,恰好现有的功能又作为公共接口在很多地方被调用。
可能的实现方式:
1. 调整代码,重新定义需要修改功能对应的函数
这需要伤筋动骨了,重点是需要确保代码的一致性,另外有可能重新定义后原来的函数调用方式没法装载新功能。要知道这是在线上稳定运行的系统呀!
2. 把新功能封装成可接收函数作为参数、同时调用原函数的高阶函数,然后通过嵌套函数来调用返回高阶函数
这就需要用到今天的主角装饰器了。
三、装饰器的定义
顾名思义,装饰器是用来装饰的,它本身也是一个函数,只不过接收其它函数作为参数并装饰其它函数,为其它函数提供额外的附加功能。最直接的定义是,装饰器其实就是一个接收函数作为参数,并返回一个替换函数的可执行函数(详情参照下文论述)。
四、装饰器的作用和应用场景
上文已经大概提到,装饰器是装饰其他函数的,为其他函数提供原本没有的附加功能。引用一段比较详细的文字:装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。
装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
五、定义和使用装饰器的原则
定义和使用装饰器需遵循以下原则:
- 不能修改被装饰的函数的源代码
- 不能改变被装饰函数的调用方式
以上两点是为了保障被装饰函数的一致性和维护性,以及新增功能的扩展性和可重用性(与原函数无关)。
六、装饰器的本质
先来逐个梳理以下要点:
- 装饰器如何装饰其他函数?
通过高阶函数的特性,把被装饰的函数作为参数传递到装饰器内部,然后在装饰器内部嵌套定义一个专门用于装饰的函数,该函数在实现对被装饰函数的调用执行的同时,封装实现需要添加的额外功能。 - 装饰器如何实现不改变对被装饰函数的调用形式?
在装饰器内部调用被装饰的函数时,就像未引入装饰器概念一样简简单单地调用被装饰的函数即可。 - 为什么讲装饰器会返回一个替换函数?
装饰器本身也是一个函数,虽然它已经调用了被装饰的函数,且封装实现了需要添加的额外功能,但我们要使用它也需要像普通函数一样去调用执行才行。此外,调用执行后要达到我们的预期目的,装饰器的返回值需要包含对被装饰函数的调用执行和额外添加功能的实现。预备知识已经阐述过,对变量的调用时通过对变量名保存的内存空间地址的引用来实现的,因此这里可以直接返回装饰器的函数名(内存空间地址),以便后续在需要的地方直接通过调用符号()来调用实现。
当我们把需要被装饰的函数传递给装饰器后,被装饰的函数本质上发生了革命性的变化,即foo=wrapper(foo), 虽然与被装饰之前名称看着相同,但实质内容是返回的被装饰后的函数,即返回了一个替换函数。 - 为什么装饰器都至少需要双层嵌套函数呢?
查询资料就可以发现讲解装饰器时的程序示例中的装饰器都至少设计了两层嵌套函数,外部的那层用于把被装饰的函数作为参数传递进去,内部的那层才是真正的装饰所用。直接把两层合二为一,在一个函数内部合并装饰功能难道就不行吗?
举例验证一下:
先来正统版装饰器吧:
1 import time 2 3 4 def timmer(func): #外层函数传递被装饰的函数 5 def warpper(*args,**kwargs): 6 start_time=time.time() 7 func() #保持原样调用被装饰的函数 8 stop_time=time.time() 9 print('the func run time is %s' %(stop_time-start_time)) 10 return warpper #把内层实际调用执行被装饰的函数以及封装额外装饰功能的函数名(内存空间地址)作为返回值返回,以便后续调用执行 11 12 @timmer 13 def test1(): 14 time.sleep(3) 15 print('in the test1') 16 17 test1() 18 19 程序输出: 20 in the test1 #原生方式调用执行被装饰的函数 21 the func run time is 3.000171661376953 #附加了额外的程序执行时间统计功能
请注意这里定义的warpper函数的参数形式,非固定参数意味着实际传入的被装饰的函数可以有参数也可以没有参数。
现在我们把上述双层嵌套函数改装成一个函数来试试:
1 import time 2 def timmer(func): 3 start_time=time.time() 4 func() 5 stop_time=time.time() 6 print('the func run time is %s' %(stop_time-start_time)) 7 return timmer #去掉内层嵌套函数warpper,直接返回timmer自身 8 9 10 @timmer 11 12 13 def test1(): 14 time.sleep(3) 15 print('in the test1') 16 test1() 17 18 程序输出: 19 in the test1 20 Traceback (most recent call last): 21 the func run time is 3.000171661376953 22 File "E:/Python_Programs/S13/day4/deco.py", line 20, in <module> 23 test1() 24 TypeError: timmer() missing 1 required positional argument: 'func'
请注意我们改编装饰器后程序虽然可以运行,但已然报错了,提示最后一行在调用test1这个被装饰的函数时少了一个位置参数func:改编后的程序的返回值
是timmer本身,而我们在定义timmer函数时已经为其定义了一个参数func,因此报出缺少参数错误。
关于这个参数错误,我们同样可以通过修改内层嵌套函数的参数形式来佐证一下:
1 import time 2 def timmer(func): 3 def warpper(x): #这里故意为内层函数定义一个参数 4 print(x) 5 start_time=time.time() 6 func() 7 stop_time=time.time() 8 print('the func run time is %s' %(stop_time-start_time)) 9 return warpper 10 11 @timmer 12 def test1(): 13 time.sleep(3) 14 print('in the test1') 15 16 test1() 17 18 程序输出: 19 Traceback (most recent call last): 20 File "E:/Python_Programs/S13/day4/deco2.py", line 18, in <module> 21 test1() 22 TypeError: warpper() missing 1 required positional argument: 'x'
可以看出我们修改内层函数的参数定义后会报相同的错误,而且直接导致程序不能运行了!我们第一个演示程序中内层函数的参数是非固定参数,可有可无,因此运行OK。
还记得上文强调的装饰器的原则么?一是不改变对被装饰函数的调用执行方式(就要原生态调用);二是不改变被装饰函数的源代码。这改编后的装饰器的问题就在于不能满足第一条原生态调用被
装饰函数的条件了。要修复这个问题,我们只能返回一个不带任何参数或者说可以不带参数的函数作为返回值,而现状是装饰器函数本身已经被固化了,必须且只能传入func一个参数以便将被装饰
的函数传递给装饰器,因此我们不得不引入一个不带参数的内嵌函数,用它来完成需要的装饰并作为返回值以便后续调用。
于是装饰器就变成两层嵌套函数,外层(第一层)函数负责把需要被装饰的函数作为参数传递到装饰器内部,并定义整个装饰器的返回值,内层(第二层)函数负责执行具体的装饰功能,而外层定
义的返回值就是内层实际原生态调用被装饰函数和执行额外装饰功能的内层嵌套函数。两层嵌套分工明确又相得益彰,仔细推敲下这设计模式真是太nb了!
这也是很多地方说高阶函数+嵌套函数=>装饰器的原因。
在此也附上网上某大神的解答:
- 为什么装饰器的返回值一定要在外层函数中定义?在内层函数中定义可以吗?
先复习下函数的返回值有关概念,如果没有定义return值,那么函数会返回none,此时函数的type是NoneType。当我们在内嵌函数中定义返回值来替代在外层函数中定义返回值时,外层函数就没有return值,本身类型变成NoneType,实际调用时程序会报“TypeError: 'NoneType' object is not callable”的错误。一个既定的事实是,尽管实际执行装饰功能的函数是内层函数,但我们在调用时还是调用的外层函数,否则被装饰的函数又没法传递给装饰器了!
OK,到这里了再回顾下装饰器的几个要点是不是有种步步惊心、环环相扣、天衣无缝的赶脚? - 语法糖@又是个什么东东呢?
稍微注意一下细节不难发现,装饰器定义后会通过@decorator的方式来调用,这一声明往往在被装饰的函数前面的位置出现。这就是传说中的语法糖。比如上文中的示例程序,@timmer,完全等价于test1=timmer(test1), 这也是装饰器会返回一个替换函数的精髓所在。这里的有两个细节需要注意:
1. 对test1进行赋值时是通过引用方式传递的一个函数名(门牌号,内存空间地址);
2. 下文还需要通过调用方式才能真正实现对test1的装饰,调用方法就是test1加上调用符号()了
但是请注意,语法糖并不是什么高大上的东东,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。
至此,装饰器的一些要点已经阐述完毕,是时候对装饰器作一些总结了。
装饰器总结:
装饰器本身也是一个可执行函数,只不过是一个高阶函数;
装饰器通过接收函数作为参数,结合嵌套函数来实现对被装饰函数的装饰处理;
装饰器的嵌套函数至少应该有两层,外层接收被装饰的函数作为参数,传递给内层,并将内层函数(替换函数)return回去以便后续调用装饰;内层完成对被装饰的函数的原生态调用和自定义的额外装饰功能;
装饰器返回的替换函数的本质在于嵌套函数的内层不仅实现了对被装饰函数的原生态调用,且额外增加了预期装饰的功能,调用装饰器的过程中在不改变被装饰函数名称(变量名)的前提下,改变了函数体(变量的值);
装饰器本身只能接收被装饰的函数作为唯一的参数,但是可以在内层函数中定义额外参数来实现对带参数的函数进行装饰的目的,同时还可以再在外层增加一层嵌套,为装饰器定义其它的参数(具体在下文程序示例中会演示);
装饰器的作用全部体现在装饰二字上。
七、装饰器程序示例(装饰无参数函数、装饰有参数函数、装饰器自带参数)
- 装饰无参数的函数
来一个再普通不同的栗子吧:
1 def deco1(func): #外层函数把被装饰的函数作为参数引入 2 def wrapper(): 3 print('Begin----') 4 func() #内嵌函数开始调用执行被装饰的函数,调用方式是原生态的 5 print('End-----') 6 return wrapper # 此处返回的函数即为替换函数,包含了对原函数调用执行和增加额外装饰功能的逻辑,注意这里返回的是函数名(门牌号) 7 8 9 @deco1 10 def test1(): 11 print('This is for test') 12 13 test1() #这里的test1在执行时会被替换为装饰器中的wrapper函数,并非原本意义上定义的test1函数了,原本意义上定义的test1函数对应于与wrapper中的func()
#这里通过调用符号()来调用执行被替换后的test1函数,请注意装饰器中的返回值是wrapper即替换函数的内存空间地址(门牌号),通过调用符号()即可获取
函数体(对变量test1进行赋值处理) 14 15 程序输出: 16 Begin---- 17 This is for test 18 End----- - 装饰有参数的函数
装饰有参数的函数时,参数需要在装饰器的内层函数中接收。
为了直观演示相关逻辑,直接上一个非固定参数的栗子:
1 __author__ = 'Beyondi' 2 #!/usr/bin/env python 3 #! -*- coding:utf-8 -*- 4 5 6 def dec2(func): #外层函数只能处理被装饰的函数这一个参数 7 def wrapper(*args, **kwargs): #被装饰的函数的参数,一定要在内嵌函数中引入处理 8 print('Begin to decorate...') 9 ret = func(*args, **kwargs) 10 print('Arguments are %s %s' % (args, kwargs)) 11 return ret 12 return wrapper 13 14 15 @dec2 16 def test2(x, y, z): 17 print('aaa') 18 return 2 19 20 test2('a', 'b', z='c') #实际调用时被装饰的函数参数传递方式不变 21 22 程序输出: 23 Begin to decorate... 24 aaa 25 Arguments are ('a', 'b') {'z': 'c'} 26
搞定了非固定参数的函数装饰,固定参数的函数装饰当然更简单了。 - 装饰器自带参数
上面的栗子的关注点都在被装饰的函数是否带参数,实际应用中要让装饰器的功能更强大全面,往往需要给装饰器也定义参数,以便执行更复杂灵活的逻辑。以下示例程序就在上述程序基础上对被装饰的函数传递的实参长度进行判断处理:
1 def deco(limit): #装饰器自带的参数需要再定义一个外部函数来引入 2 def dec2(func): #接收处理被装饰的函数变量 3 def wrapper(*args, **kwargs): #处理被装饰的函数传递的参数,逻辑不变 4 print('Begin to decorate...') 5 # print(args) 6 func(*args, **kwargs) 7 if len(args) >= limit: #装饰器自带的参数开始派上用场 8 print('Arguments OK') 9 else: 10 print('Arguemts error') 11 return wrapper 12 return dec2 13 14 15 @deco(2) #通过语法糖进行装饰时,需要把装饰器自带的参数传递进去,改变这个实参会影响程序最后的输出结果 16 def test2(x, y, z): 17 print('aaa') 18 return 2 19 20 test2('a', 'b', z='c') 21 22 程序输出: 23 Begin to decorate... 24 aaa 25 Arguments OK # 程序输出结果符合预期
以上程序表明,给装饰器本身引入参数可实现更灵活强大的装饰效果。需要注意的是装饰器自己的参数一定要在装饰器的最外层定义引入,此时真正的装饰器
就是最里层嵌套的函数了。这也是为什么讲装饰器至少是需要双层嵌套的高阶函数。