一篇文章掌握 Python 中的装饰器

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)。再加上围绕他的计时功能,故而无损增加了计时功能。

综上,理解装饰器最重要的是将@ 语句和赋值语句等同起来。同时需要理解被装饰函数作为参数传入装饰器,嵌套函数对其进行改造,最后作为函数返回,使被装饰函数实质上关联到新函数上。

上一篇:Linux从程序到进程


下一篇:【Java基础学习笔记】8、内部类