python中的装饰器用于修饰函数,以增强函数的行为:记录函数执行时间,建立和撤销环境,记录日志等。装饰器可以在不修改函数内部代码的前提下实现以上增强行为。如下代码建立一个计时装饰器,随后描述其工作原理。
import time
def timethis(func):
def inner(*args,**kwargs):
print('start timer:')
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print('end timer:%fs.'%(end - start))
return result
return inner
@timethis
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
print(sleeps(3))
执行以上代码,输出:
start timer:
sleeps begin:
sleep 3 seconds.
sleeps over.
end timer:3.002512s.
3
可见,timethis装饰器实现了为sleeps函数计时的功能。其关键在于@标识符的使用。
一、理解@标识符
@标识符是Pyton的语法糖,定义被装饰函数时使用@timethis修饰和用语句sleeps = timethis(sleeps)是等价的。
@timethis
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
相当于
'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:531509025
寻找有志同道合的小伙伴,互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
sleeps = timethis(sleeps)
@语法只是装饰器调用的便捷方式:将被装饰函数sleeps作为参数传给装饰器函数,再将装饰器返回值重新绑定到原sleeps变量上。理解了装饰器的使用方法,我们一步步来理解其定义过程。
二、装饰器是一个函数
- 根据上文timethis装饰器的定义,它毫无疑问是一个函数。名称是timethis,参数是func,返回值是inner。
- 根据 sleeps = timethis(sleeps) ,可知参数func是被装饰的函数sleeps。
- 根据 return inner ,可知返回值inner是嵌套定义在装饰器中的一个函数。
综上,装饰器本身是一个函数,参数也是函数,返回值还是函数。之所以函数可以作为装饰器的参数和返回值,是因为函数在Python中是一等对象。
三、函数是一等对象
编程语言中的一等对象定义为:运行时创建,可赋值给变量或数据结构,可作为参数传递,可作为返回值返回。
Python中整数、字符串、字典类型是一等对象,具备以上四点特性,理解起来没有任何困难。但函数作为一等对象,需要我们举例说明。
3.1运行时创建
在Python控制台中定义一个函数reverse,实现对word这个序列类型的反转。
>>> def reverse(word):
... return word[::-1]
...
>>> reverse
<function reverse at 0x027A4C40>
>>> reverse('hello world!')
'!dlrow olleh'
因其是在控制台会话中定义的,符合第一条运行时创建的要求。
3.2可赋值给变量或数据结构
可以将reverse函数赋值给另外的变量,再调用。如
>>> backward=reverse
>>> backward('hello world!')
'!dlrow olleh'
输出结果同上。所以函数符合第二条可赋值给变量的要求。
3.3函数作为参数传递
当使用高阶函数,如sorted时,高阶函数的key关键字接受一个单参数函数,对每个元素进行迭代,依照这个key函数作为排序依 据。
cars = ['Honda','toyota','hyundai','byd','ford','suzuki','peuguot','nissan','citroen','kia','vw','gm','audi','bmw','beniz']
print(sorted(cars,key=reverse))
输出
['Honda', 'kia', 'toyota', 'ford', 'byd', 'hyundai', 'audi', 'suzuki', 'gm', 'nissan', 'citroen', 'peuguot', 'bmw', 'vw', 'beniz']
此时所有的car是依照结尾字符的先后排序的。reverse作为参数传入高阶函数。符合第三条函数可作为参数传递。
3.4函数作为返回值返回
为验证第四点,我们将reverse函数包装起来,让他在一个函数中返回。
'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:531509025
寻找有志同道合的小伙伴,互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
def cmpLib():
def reverse(word):
return word[::-1]
return reverse
我们仍用上例中排序函数,key参数必须为一个单参函数。而函数backward的执行结果是一个函数,所以我们把它的调用结果作为key值。
print(sorted(cars,key=cmpLib())
结果
['Honda', 'kia', 'toyota', 'ford', 'byd', 'hyundai', 'audi', 'suzuki', 'gm', 'nissan', 'citroen', 'peuguot', 'bmw', 'vw', 'beniz']
可见,结果正确。所以第四条函数可作为结果返回也成立。
综上,函数是一等对象。除了可调用性之外,函数和其他如字典、字符串、列表对象并没有本质区别。
理解装饰器我们需要的是函数一等性定义的后三点:函数可赋值,可作参数,可作返回结果。
我们再来分析与@timethis等价的sleeps = timethis(sleeps)
语句:右侧函数先调用。timethis是装饰器函数,被装饰函数sleeps作为参数传入装饰器中;返回结果是装饰器中定义的inner函数;右侧计算结果重新赋值给变量sleeps。完美符合以上三点。也就是说sleeps函数实际上已经指向inner函数了。
理解了函数一等性,就理解了函数可以作为参数传递和作为结果返回。那么新定义的内部函数inner为什么采用 def inner(*args,**kwargs):
的参数命名形式呢?
四、可接受任意数量参数的函数
当我们定义不特定数量参数的函数时,可使用*开头的参数作可接受任意数量位置参数的参数,此时该参数作为一个元组使用。
同理,可以使用**
开头的关键字参数接受任意数量的关键词参数,此时该参数作为一个字典使用。
如果同时接受任意数量的位置参数和关键字参数,那么只要联合使用*
和 **
就可以。而 def inner(*args,**kwargs):
是约定俗成的固定写法。来看个例子就可以理解这种写法了。
def star(*args,**kwargs):
print(args,kwargs)
star(1,2,3)
star(4,5,name='zhang')
star(7,name='lisi',gender='m')
输出结果:
(1, 2, 3) {}
(4, 5) {'name': 'zhang'}
(7,) {'name': 'lisi', 'gender': 'm'}
args搜集所有位置参数,kwargs搜集所有关键字参数。这个技术应用在inner函数上,恰如其分:当我们调用@语法时,只有被装饰函数sleeps作为func参数传入timethis装饰器中,sleeps的参数并没有传入装饰器函数中。装饰器不知道sleeps函数的参数数量和具体值,若在其中func调用参数,则相当于调用不特定名称和数量的参数。
接受任意参数的inner函数,进一步将参数传给在其中执行的func函数。func函数是被装饰的原函数sleeps,传给inner函数的*args
,**kwargs
参数,直接传递给了被装饰函数func。这样就实现了func(*args,**kwargs)
相当于sleeps(3)的效果。
在完成调用原函数的基础上,如何添加计时功能的呢?
五、增强被装饰函数的行为
以下语句实现了统计函数执行时间的功能 ,当然也可以实现比如日志记录,建立撤销环境之类的功能,大同小异。
print('start timer:')
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print('end timer:%fs.'%(end - start))
很简单,就是在调用原函数的语句 result = func(*args,**kwargs)
前后,包裹上相应的计时功能。
此处func参数得以在inner内部访问到,还牵涉到一个不太好理解的话题——闭包,而理解闭包需要先弄清python中变量的作用域规则。
六、变量作用域
Python变量分全局变量,局部变量。另外函数的参数是函数的局部变量。编写如下代码:
'''
遇到问题没人解答?小编创建了一个Python学习交流QQ群:531509025
寻找有志同道合的小伙伴,互帮互助,群里还有不错的视频学习教程和PDF电子书!
'''
b=3
def func(a):
print(a)
print(b)
b=2
func(2)
让我们猜猜运行结果,应该是1,3对吧,但执行却提示出错:
File "dec.py", line 47, in func
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
提示先用但未赋值。但b是全局变量,一般理解不论是print(b)对全局变量的读取,还是b=2对全局变量的赋值,都不会出现这个问题。
问题出在b=2语句上,Python对在函数定义体中 赋值 的变量都认为是局部变量。从而导致局部变量b未赋值先使用的问题。
为解决这个问题,若在函数中重新赋值了全局变量,需要在函数中使用global声明其为全局变量。即函数若读取全局变量,可以直接使用。但若在函数体中重新赋值全局变量,那就需要global声明变量是全局变量。
新问题出现了:在装饰器timethis中,func是其参数,也就是局部变量,这是无疑的。那么在inner函数中是怎么访问func的呢?这就牵涉到闭包问题了。
七、闭包
闭包指延伸作用域的函数,函访可访问定义体之外定义的非全局变量。在例子中,timethis的参数func就未定义在inner函数中,而且也不是全局变量。是闭包将其延伸到了inner函数中,作为自由变量来使用。所以闭包是一种函数,保留了它在定义时存在的自由变量。本例中,闭包从timethis定义行到return inner这个范围,此时的局部变量func对于闭包中的inner函数来说,就是自由变量,可以读取和使用。但不可在其中对自由变量赋值。
类似于全局变量,当我们在嵌套的函数中对自由变量访问时,可以自由使用。但是当我们重新对其赋值时,解释器会把这个值视为一个局部变量。若需赋值全局变量,需引入global声明全局变量;若需赋值自由变量,需引入nonlocal声明自由变量。
因此,func是作为自由变量被闭包函数inner使用的。那么以上语句之后为什么有两个返回语句呢?
八、返回值和返回函数
-
第一个返回值,返回的是func的执行结果,它属于inner函数的返回值,等效于sleeps函数的返回值,这是sleep函数的应有之意。保持了原函数sleeps对外结构的一致性。
-
第二个返回值是inner函数本身,也就是第三部分讲述的函数作为返回结果的用法。依据
slepps=timethi(sleeps)
语法,其返回结果是inner函数,传递给sleeps函数,使sleeps函数实际上等同于inner函数。所以调用sleeps(3)相当于调用inner(3)。再加上围绕他的计时功能,故而无损增加了计时功能。
综上,理解装饰器最重要的是将@ 语句和赋值语句等同起来。同时需要理解被装饰函数作为参数传入装饰器,嵌套函数对其进行改造,最后作为函数返回,使被装饰函数实质上关联到新函数上。