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
-
相当于
-
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函数包装起来,让他在一个函数中返回。
-
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变量分全局变量,局部变量。另外函数的参数是函数的局部变量。编写如下代码:
-
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)。再加上围绕他的计时功能,故而无损增加了计时功能。
综上,理解装饰器最重要的是将@ 语句和赋值语句等同起来。同时需要理解被装饰函数作为参数传入装饰器,嵌套函数对其进行改造,最后作为函数返回,使被装饰函数实质上关联到新函数上。
作者:巩庆奎,大奎,对计算机、电子信息工程感兴趣。gongqingkui at 126.com
赞 赏 作 者
更多阅读
特别推荐
点击下方阅读原文加入社区会员
转载:https://blog.csdn.net/BF02jgtRS00XKtCx/article/details/113577228