5分钟掌握 Python 中的装饰器

Python中文社区

共 5576字,需浏览 12分钟

 ·

2021-02-02 08:50

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

赞 赏 作 者



更多阅读



2020 年最佳流行 Python 库 Top 10


2020 Python中文社区热门文章 Top 10


5分钟快速掌握 Python 定时任务框架

特别推荐




点击下方阅读原文加入社区会员

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报