Python函数装饰器实现方法详解


Posted in Python onDecember 22, 2018

本文实例讲述了Python函数装饰器实现方法。分享给大家供大家参考,具体如下:

编写函数装饰器

这里主要介绍编写函数装饰器的相关内容。

跟踪调用

如下代码定义并应用一个函数装饰器,来统计对装饰的函数的调用次数,并且针对每一次调用打印跟踪信息。

class tracer:
  def __init__(self,func):
    self.calls = 0
    self.func = func
  def __call__(self,*args):
    self.calls += 1
    print('call %s to %s' %(self.calls, self.func.__name__))
    self.func(*args)
@tracer
def spam(a, b, c):
  print(a + b + c)

这是一个通过类装饰的语法写成的装饰器,测试如下:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam('a','b','c')
call 2 to spam
abc
>>> spam.calls
2
>>> spam
<__main__.tracer object at 0x03098410>

运行的时候,tracer类和装饰的函数分开保存,并且拦截对装饰的函数的随后的调用,以便添加一个逻辑层来统计和打印每次调用。

装饰之后,spam实际上是tracer类的一个实例。

@装饰器语法避免了直接地意外调用最初的函数。考虑如下所示的非装饰器的对等代码:

calls = 0
def tracer(func,*args):
  global calls
  calls += 1
  print('call %s to %s'%(calls,func.__name__))
  func(*args)
def spam(a,b,c):
  print(a+b+c)

测试如下:

>>> spam(1,2,3)
6
>>> tracer(spam,1,2,3)
call 1 to spam
6

这一替代方法可以用在任何函数上,且不需要特殊的@语法,但是和装饰器版本不同,它在代码中调用函数的每个地方都需要额外的语法。尽管装饰器不是必需的,但是它们通常是最为方便的。

扩展——支持关键字参数

下述代码时前面例子的扩展版本,添加了对关键字参数的支持:

class tracer:
  def __init__(self,func):
    self.calls = 0
    self.func = func
  def __call__(self,*args,**kargs):
    self.calls += 1
    print('call %s to %s' %(self.calls, self.func.__name__))
    self.func(*args,**kargs)
@tracer
def spam(a, b, c):
  print(a + b + c)
@tracer
def egg(x,y):
  print(x**y)

测试如下:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam(a=4,b=5,c=6)
call 2 to spam
15
>>> egg(2,16)
call 1 to egg
65536
>>> egg(4,y=4)
call 2 to egg
256

也可以看到,这里的代码同样使用【类实例属性】来保存状态,即调用的次数self.calls。包装的函数和调用计数器都是针对每个实例的信息。

使用def函数语法写装饰器

使用def定义装饰器函数也可以实现相同的效果。但是有一个问题,我们也需要封闭作用域中的一个计数器,它随着每次调用而更改。我们可以很自然地想到全局变量,如下:

calls = 0
def tracer(func):
  def wrapper(*args,**kargs):
    global calls
    calls += 1
    print('call %s to %s'%(calls,func.__name__))
    return func(*args,**kargs)
  return wrapper
@tracer
def spam(a,b,c):
  print(a+b+c)
@tracer
def egg(x,y):
  print(x**y)

这里calls定义为全局变量,它是跨程序的,是属于整个模块的,而不是针对每个函数的,这样的话,对于任何跟踪的函数调用,计数器都会递增,如下测试:

>>> spam(1,2,3)
call 1 to spam
6
>>> spam(a=4,b=5,c=6)
call 2 to spam
15
>>> egg(2,16)
call 3 to egg
65536
>>> egg(4,y=4)
call 4 to egg
256

可以看到针对spam函数和egg函数,程序用的是同一个计数器。

那么如何实现针对每一个函数的计数器呢,我们可以使用Python3中新增的nonlocal语句,如下:

def tracer(func):
  calls = 0
  def wrapper(*args,**kargs):
    nonlocal calls
    calls += 1
    print('call %s to %s'%(calls,func.__name__))
    return func(*args,**kargs)
  return wrapper
@tracer
def spam(a,b,c):
  print(a+b+c)
@tracer
def egg(x,y):
  print(x**y)
spam(1,2,3)
spam(a=4,b=5,c=6)
egg(2,16)
egg(4,y=4)

运行如下:

call 1 to spam
6
call 2 to spam
15
call 1 to egg
65536
call 2 to egg
256

这样,将calls变量定义在tracer函数内部,使之存在于一个封闭的函数作用域中,之后通过nonlocal语句来修改这个作用域,修改这个calls变量。如此便可以实现我们所需求的功能。

陷阱:装饰类方法

【注意,使用类编写的装饰器不能用于装饰某一类中带self参数的的函数,这一点在Python装饰器基础中介绍过】

即如果装饰器是如下使用类编写的:

class tracer:
  def __init__(self,func):
    self.calls = 0
    self.func = func
  def __call__(self,*args,**kargs):
    self.calls += 1
    print('call %s to %s'%(self.calls,self.func.__name__))
    return self.func(*args,**kargs)

当它装饰如下在类中的方法时:

class Person:
  def __init__(self,name,pay):
    self.name = name
    self.pay = pay
  @tracer
  def giveRaise(self,percent):
    self.pay *= (1.0 + percent)

这时程序肯定会出错。问题的根源在于,tracer类的__call__方法的self——它是一个tracer实例,当我们用__call__把装饰方法名重绑定到一个类实例对象的时候,Python只向self传递了tracer实例,它根本没有在参数列表中传递Person主体。此外,由于tracer不知道我们要用方法调用处理的Person实例的任何信息,没有办法创建一个带有一个实例的绑定的方法,所以也就没有办法正确地分配调用。

这时我们只能通过嵌套函数的方法来编写装饰器。

计时调用

下面这个装饰器将对一个装饰的函数的调用进行计时——既有针对一次调用的时间,也有所有调用的总的时间。

import time
class timer:
  def __init__(self,func):
    self.func = func
    self.alltime = 0
  def __call__(self,*args,**kargs):
    start = time.clock()
    result = self.func(*args,**kargs)
    elapsed = time.clock()- start
    self.alltime += elapsed
    print('%s:%.5f,%.5f'%(self.func.__name__,elapsed,self.alltime))
    return result
@timer
def listcomp(N):
  return [x*2 for x in range(N)]
@timer
def mapcall(N):
  return list(map((lambda x :x*2),range(N)))
result = listcomp(5)
listcomp(50000)
listcomp(500000)
listcomp(1000000)
print(result)
print('allTime = %s'%listcomp.alltime)
print('')
result = mapcall(5)
mapcall(50000)
mapcall(500000)
mapcall(1000000)
print(result)
print('allTime = %s'%mapcall.alltime)
print('map/comp = %s '% round(mapcall.alltime/listcomp.alltime,3))

运行结果如下:

listcomp:0.00001,0.00001
listcomp:0.00885,0.00886
listcomp:0.05935,0.06821
listcomp:0.11445,0.18266
[0, 2, 4, 6, 8]
allTime = 0.18266365607537918
mapcall:0.00002,0.00002
mapcall:0.00689,0.00690
mapcall:0.08348,0.09038
mapcall:0.16906,0.25944
[0, 2, 4, 6, 8]
allTime = 0.2594409060462425
map/comp = 1.42

这里要注意的是,map操作在Python3中返回一个迭代器,所以它的map操作不能和一个列表解析的工作直接对应,即实际上它并不花时间。所以要使用list(map())来迫使它像列表解析那样构建一个列表

添加装饰器参数

有时我们需要装饰器来做一个额外的工作,比如提供一个输出标签并且可以打开或关闭跟踪消息。这就需要用到装饰器参数了,我们可以使用装饰器参数来制定配置选项,这些选项可以根据每个装饰的函数而编码。例如,像下面这样添加标签:

def timer(label = ''):
  def decorator(func):
    def onCall(*args):
      ...
      print(label,...)
    return onCall
  return decorator
@timer('==>')
def listcomp(N):...

我们可以将这样的结果用于计时器中,来允许在装饰的时候传入一个标签和一个跟踪控制标志。比如,下面这段代码:

import time
def timer(label= '', trace=True):
  class Timer:
    def __init__(self,func):
      self.func = func
      self.alltime = 0
    def __call__(self,*args,**kargs):
      start = time.clock()
      result = self.func(*args,**kargs)
      elapsed = time.clock() - start
      self.alltime += elapsed
      if trace:
        ft = '%s %s:%.5f,%.5f'
        values = (label,self.func.__name__,elapsed,self.alltime)
        print(format % value)
      return result
  return Timer

这个计时函数装饰器可以用于任何函数,在模块中和交互模式下都可以。我们可以在交互模式下测试,如下:

>>> @timer(trace = False)
def listcomp(N):
  return [x * 2 for x in range(N)]
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> listcomp
<__main__.timer.<locals>.Timer object at 0x036DCC10>
>>> listcomp.alltime
0.0011475424533080223
>>>
>>> @timer(trace=True,label='\t=>')
def listcomp(N):
  return [x * 2 for x in range(N)]
>>> x = listcomp(5000)
  => listcomp:0.00036,0.00036
>>> x = listcomp(5000)
  => listcomp:0.00034,0.00070
>>> x = listcomp(5000)
  => listcomp:0.00034,0.00104
>>> listcomp.alltime
0.0010432902706075842

更多关于Python相关内容可查看本站专题:《Python数据结构与算法教程》、《Python Socket编程技巧总结》、《Python函数使用技巧总结》、《Python字符串操作技巧汇总》及《Python入门与进阶经典教程》

希望本文所述对大家Python程序设计有所帮助。

Python 相关文章推荐
Python实现购物系统(示例讲解)
Sep 13 Python
Python入门学习指南分享
Apr 11 Python
python用户评论标签匹配的解决方法
May 31 Python
Python2.7.10以上pip更新及其他包的安装教程
Jun 12 Python
Python从ZabbixAPI获取信息及实现Zabbix-API 监控的方法
Sep 17 Python
python 多进程共享全局变量之Manager()详解
Aug 15 Python
Pytorch提取模型特征向量保存至csv的例子
Jan 03 Python
基于Tensorflow:CPU性能分析
Feb 10 Python
Django {{ MEDIA_URL }}无法显示图片的解决方式
Apr 07 Python
python except异常处理之后不退出,解决异常继续执行的实现
Apr 25 Python
python 实现图与图之间的间距调整subplots_adjust
May 21 Python
python如何读取和存储dict()与.json格式文件
Jun 25 Python
使用python对文件中的单词进行提取的方法示例
Dec 21 #Python
Python类装饰器实现方法详解
Dec 21 #Python
Python实现的字典排序操作示例【按键名key与键值value排序】
Dec 21 #Python
Python简单获取二维数组行列数的方法示例
Dec 21 #Python
python进行TCP端口扫描的实现
Dec 21 #Python
Python实现将多个空格换为一个空格.md的方法
Dec 20 #Python
python解析json串与正则匹配对比方法
Dec 20 #Python
You might like
关于初学PHP时的知识积累总结
2013/06/07 PHP
PHP遍历文件夹与文件类及处理类用法实例
2014/09/23 PHP
php操作(删除,提取,增加)zip文件方法详解
2015/03/12 PHP
Session 失效的原因汇总及解决丢失办法
2015/09/30 PHP
php面试中关于面向对象的相关问题
2019/02/13 PHP
javascript编程起步(第四课)
2007/01/10 Javascript
WordPress 插件——CoolCode使用方法与下载
2007/07/02 Javascript
jquer之ajaxQueue简单实现代码
2011/09/15 Javascript
JS简单计算器实例
2015/01/20 Javascript
jQuery异步上传文件插件ajaxFileUpload详细介绍
2015/05/19 Javascript
利用Jquery实现几款漂亮实用的时间轴(附示例代码)
2017/02/15 Javascript
bootstrap table实现双击可编辑、添加、删除行功能
2017/09/27 Javascript
使用vue-router设置每个页面的title方法
2018/02/11 Javascript
详解vue中组件参数
2018/07/09 Javascript
JavaScript遍历数组的三种方法map、forEach与filter实例详解
2019/02/27 Javascript
页面内锚点定位及跳转方法总结(推荐)
2019/04/24 Javascript
解决node终端下运行js文件不支持ES6语法
2020/04/04 Javascript
js实现简易ATM功能
2020/10/27 Javascript
如何在 ant 的table中实现图片的渲染操作
2020/10/28 Javascript
vue切换菜单取消未完成接口请求的案例
2020/11/13 Javascript
[49:18]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 OG vs TNC
2018/04/01 DOTA
Python之PyUnit单元测试实例
2014/10/11 Python
Windows上使用virtualenv搭建Python+Flask开发环境
2016/06/07 Python
Django1.9 加载通过ImageField上传的图片方法
2018/05/25 Python
python生成n个元素的全组合方法
2018/11/13 Python
使用celery执行Django串行异步任务的方法步骤
2019/06/06 Python
Python学习笔记之Zip和Enumerate用法实例分析
2019/08/14 Python
Ubuntu18.04安装 PyCharm并使用 Anaconda 管理的Python环境
2020/04/08 Python
HTML5中的autofocus(自动聚焦)属性介绍
2014/04/23 HTML / CSS
美赞臣新加坡官方旗舰店:Enfagrow新加坡
2019/05/15 全球购物
几个人围成一圈的问题
2013/09/26 面试题
红旗方阵解说词
2014/02/12 职场文书
财务总经理岗位职责
2014/02/16 职场文书
2014年乡镇植树节活动方案
2014/02/28 职场文书
垃圾桶标语
2014/06/24 职场文书
阿里面试Nacos配置中心交互模型是push还是pull原理解析
2022/07/23 Java/Android