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代码调试的几种方法总结
Apr 15 Python
举例详解Python中threading模块的几个常用方法
Jun 18 Python
Python 读写文件和file对象的方法(推荐)
Sep 12 Python
python实现的二叉树定义与遍历算法实例
Jun 30 Python
Python中字典与恒等运算符的用法分析
Aug 22 Python
使用tensorflow显示pb模型的所有网络结点方式
Jan 23 Python
Python编程快速上手——strip()函数的正则表达式实现方法分析
Feb 29 Python
Python astype(np.float)函数使用方法解析
Jun 08 Python
使用npy转image图像并保存的实例
Jul 01 Python
python爬取招聘要求等信息实例
Nov 20 Python
PyCharm配置KBEngine快速处理代码提示冲突、配置命令问题
Apr 03 Python
python pyhs2 的安装操作
Apr 07 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
Apache 配置详解(最好的APACHE配置教程)
2010/07/04 PHP
php递归实现无限分类生成下拉列表的函数
2010/08/08 PHP
PHP中限制IP段访问、禁止IP提交表单的代码
2011/04/23 PHP
php在线代理转向代码
2012/05/05 PHP
php截取字符串并保留完整xml标签的函数代码
2013/02/06 PHP
PHP 转义使用详解
2013/07/15 PHP
php通过Chianz.com获取IP地址与地区的方法
2015/01/14 PHP
PHP遍历数组的三种方法及效率对比分析
2015/02/12 PHP
PHP封装curl的调用接口及常用函数详解
2018/05/31 PHP
PDO::prepare讲解
2019/01/29 PHP
php 多进程编程父进程的阻塞与非阻塞实例分析
2020/02/22 PHP
ff下JQuery无法监听input的keyup事件的解决方法
2013/12/12 Javascript
JQuery实现鼠标移动图片显示描述层的方法
2015/06/25 Javascript
javascript图片滑动效果实现
2021/01/28 Javascript
jQuery解析XML 详解及方法总结
2016/09/28 Javascript
微信小程序 获取设备信息 API实例详解
2016/10/02 Javascript
Vue.directive()的用法和实例详解
2018/03/04 Javascript
微信小程序实现上传word、txt、Excel、PPT等文件功能
2019/05/23 Javascript
微信小程序服务器日期格式化问题
2020/01/07 Javascript
在react中使用vue的状态管理的方法示例
2020/05/02 Javascript
Python安装第三方库及常见问题处理方法汇总
2016/09/13 Python
基于python的多进程共享变量正确打开方式
2018/04/28 Python
对python抓取需要登录网站数据的方法详解
2018/05/21 Python
python使用 zip 同时迭代多个序列示例
2019/07/06 Python
Python画图高斯分布的示例
2019/07/10 Python
Tensorflow 使用pb文件保存(恢复)模型计算图和参数实例详解
2020/02/11 Python
JAVA SWT事件四种写法实例解析
2020/06/05 Python
把Anaconda中的环境导入到Pycharm里面的方法步骤
2020/10/30 Python
CSS3制作炫酷的自定义发光文字
2016/03/28 HTML / CSS
canvas绘制树形结构可视图形的实现
2020/04/03 HTML / CSS
职务聘任书范文
2014/03/29 职场文书
中班上学期幼儿评语
2014/04/30 职场文书
市贸粮局召开党的群众路线教育实践活动总结大会新闻稿
2014/10/21 职场文书
英文慰问信范文
2015/03/24 职场文书
幼儿园教师工作总结2015
2015/04/02 职场文书
Ruby GDBM操作简介及数据存储原理
2022/04/19 Ruby