小言_互联网的博客

5分钟掌握 Python 中的装饰器

337人阅读  评论(0)

python中的装饰器用于修饰函数,以增强函数的行为:记录函数执行时间,建立和撤销环境,记录日志等。装饰器可以在不修改函数内部代码的前提下实现以上增强行为。如下代码建立一个计时装饰器,随后描述其工作原理。


   
  1. import time 
  2. def timethis( func):
  3.     def inner(*args,**kwargs):
  4.          print( 'start timer:')
  5.         start = time.time()
  6.         result =  func(*args,**kwargs) 
  7.         end = time.time()
  8.          print( 'end timer:%fs.'%(end - start))
  9.          return result 
  10.      return inner
  11. @timethis
  12. def sleeps(seconds):
  13.      print( '  sleeps begin:')
  14.     time.sleep(seconds)
  15.      print( '    sleep %d seconds.\n  sleeps over.'%seconds)
  16.      return seconds
  17. print(sleeps( 3))

执行以上代码,输出:


   
  1. start timer:
  2.   sleeps begin:
  3.     sleep  3 seconds.
  4.   sleeps over.
  5. end timer: 3.002512s.
  6. 3

可见,timethis装饰器实现了为sleeps函数计时的功能。其关键在于@标识符的使用。

一、理解@标识符

@标识符是Pyton的语法糖,定义被装饰函数时使用@timethis修饰和用语句sleeps = timethis(sleeps)是等价的。


   
  1. @timethis
  2. def sleeps(seconds):
  3.      print( '  sleeps begin:')
  4.     time.sleep(seconds)
  5.      print( '    sleep %d seconds.\n  sleeps over.'%seconds)
  6.      return seconds

相当于


   
  1. def sleeps(seconds):
  2.      print( '  sleeps begin:')
  3.     time.sleep(seconds)
  4.      print( '    sleep %d seconds.\n  sleeps over.'%seconds)
  5.      return seconds
  6.     
  7. sleeps = timethis(sleeps)

@语法只是装饰器调用的便捷方式:将被装饰函数sleeps作为参数传给装饰器函数,再将装饰器返回值重新绑定到原sleeps变量上。理解了装饰器的使用方法,我们一步步来理解其定义过程。

二、装饰器是一个函数

  • 根据上文timethis装饰器的定义,它毫无疑问是一个函数。名称是timethis,参数是func,返回值是inner。

  • 根据 sleeps = timethis(sleeps),可知参数func是被装饰的函数sleeps。

  • 根据return inner,可知返回值inner是嵌套定义在装饰器中的一个函数。

综上,装饰器本身是一个函数,参数也是函数,返回值还是函数。之所以函数可以作为装饰器的参数和返回值,是因为函数在Python中是一等对象。

三、函数是一等对象

编程语言中的一等对象定义为:运行时创建,可赋值给变量或数据结构,可作为参数传递,可作为返回值返回。

Python中整数、字符串、字典类型是一等对象,具备以上四点特性,理解起来没有任何困难。但函数作为一等对象,需要我们举例说明。

3.1运行时创建

在Python控制台中定义一个函数reverse,实现对word这个序列类型的反转。


   
  1. >>> def reverse(word):
  2. ...      return word[:: -1]
  3. ...
  4. >>> reverse
  5. <function reverse at  0x027A4C40>
  6. >>> reverse( 'hello world!')
  7. '!dlrow olleh'

因其是在控制台会话中定义的,符合第一条运行时创建的要求。

3.2可赋值给变量或数据结构

可以将reverse函数赋值给另外的变量,再调用。如


   
  1. >>> backward=reverse
  2. >>> backward( 'hello world!')
  3. '!dlrow olleh'

输出结果同上。所以函数符合第二条可赋值给变量的要求。

3.3函数作为参数传递

当使用高阶函数,如sorted时,高阶函数的key关键字接受一个单参数函数,对每个元素进行迭代,依照这个key函数作为排序依据。


   
  1. cars = [ 'Honda', 'toyota', 'hyundai', 'byd', 'ford', 'suzuki', 'peuguot', 'nissan', 'citroen', 'kia', 'vw', 'gm', 'audi', 'bmw', 'beniz']
  2. 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函数包装起来,让他在一个函数中返回。


   
  1. def cmpLib():
  2.     def reverse(word):
  3.          return word[:: -1]
  4.      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): 是约定俗成的固定写法。来看个例子就可以理解这种写法了。


   
  1. def star(*args,**kwargs):
  2.      print(args,kwargs)
  3. star( 1, 2, 3)
  4. star( 4, 5,name= 'zhang')
  5. star( 7,name= 'lisi',gender= 'm')

输出结果:


   
  1. ( 123) {}
  2. ( 45) { 'name''zhang'}
  3. ( 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)的效果。

在完成调用原函数的基础上,如何添加计时功能的呢?

五、增强被装饰函数的行为

以下语句实现了统计函数执行时间的功能 ,当然也可以实现比如日志记录,建立撤销环境之类的功能,大同小异。


   
  1. print( 'start timer:')
  2. start = time.time()
  3. result =  func(*args,**kwargs) 
  4. end = time.time()
  5. print( 'end timer:%fs.'%(end - start))

很简单,就是在调用原函数的语句result = func(*args,**kwargs)前后,包裹上相应的计时功能。

此处func参数得以在inner内部访问到,还牵涉到一个不太好理解的话题——闭包,而理解闭包需要先弄清python中变量的作用域规则。

六、变量作用域

Python变量分全局变量,局部变量。另外函数的参数是函数的局部变量。编写如下代码:


   
  1. b= 3
  2. def  func(a):
  3.      print(a)
  4.      print(b)
  5.     b= 2
  6. func(2)

让我们猜猜运行结果,应该是1,3对吧,但执行却提示出错:


   
  1. File  "dec.py", line  47, in  func
  2.      print(b)
  3. 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 定时任务框架

特别推荐


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


转载:https://blog.csdn.net/BF02jgtRS00XKtCx/article/details/113577228
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场