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采集腾讯新闻实例
Jul 10 Python
python多线程用法实例详解
Jan 15 Python
django 发送手机验证码的示例代码
Apr 25 Python
Python连接Mssql基础教程之Python库pymssql
Sep 16 Python
python实现反转部分单向链表
Sep 27 Python
python调用外部程序的实操步骤
Mar 04 Python
Python OpenCV中的resize()函数的使用
Jun 20 Python
python实现五子棋游戏(pygame版)
Jan 19 Python
Python 炫技操作之合并字典的七种方法
Apr 10 Python
Python2.6版本pip安装步骤解析
Aug 17 Python
使用Python webdriver图书馆抢座自动预约的正确方法
Mar 04 Python
opencv实现图像平移效果
Mar 24 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
咖啡冲泡指南 咖啡有哪些制作方式 单品咖啡 意式咖啡
2021/03/06 冲泡冲煮
PHP生成静态页
2006/11/25 PHP
基于curl数据采集之单页面采集函数get_html的使用
2013/04/28 PHP
php生成图形(Libchart)实例
2013/11/06 PHP
PHP循环输出指定目录下的所有文件和文件夹路径例子(简单实用)
2014/05/10 PHP
Yii中CGridView关联表搜索排序方法实例详解
2014/12/03 PHP
PHP中-&gt;和=&gt;的含义及使用示例解析
2020/08/06 PHP
Extjs中常用表单介绍与应用
2010/06/07 Javascript
JS解析json数据并将json字符串转化为数组的实现方法
2012/12/25 Javascript
火狐textarea输入法的bug的触发及解决
2013/07/24 Javascript
js设置cookie过期当前时间减去一秒相当于立即过期
2014/09/04 Javascript
nodejs事件的监听与触发的理解分析
2015/02/12 NodeJs
Jquery+Ajax+PHP+MySQL实现分类列表管理(下)
2015/10/28 Javascript
BootStrap实现邮件列表的分页和模态框添加邮件的功能
2016/10/13 Javascript
webpack将js打包后的map文件详解
2018/02/22 Javascript
详解Vue文档中几个易忽视部分的剖析
2018/03/24 Javascript
H5+C3+JS实现五子棋游戏(AI篇)
2020/05/28 Javascript
python中wx将图标显示在右下角的脚本代码
2013/03/08 Python
python读取浮点数和读取文本文件示例
2014/05/06 Python
Python的dict字典结构操作方法学习笔记
2016/05/07 Python
Python中shutil模块的常用文件操作函数用法示例
2016/07/05 Python
聊聊Python中的pypy
2018/01/12 Python
python实现排序算法解析
2018/09/08 Python
python占位符输入方式实例
2019/05/27 Python
Python基于Faker假数据构造库
2020/11/30 Python
CSS3结构性伪类选择器九种写法
2012/04/18 HTML / CSS
俄罗斯卫浴采暖及维修用品超级市场:Dkrussia
2020/05/12 全球购物
介绍一下Linux文件的记录形式
2013/09/29 面试题
如何通过jdbc调用存储过程
2012/04/19 面试题
回门宴新郎答谢词
2014/01/12 职场文书
奥巴马开学演讲稿
2014/05/15 职场文书
12.4法制宣传日活动总结
2014/08/26 职场文书
酒店保洁员岗位职责
2015/02/26 职场文书
教务处教学工作总结
2015/08/10 职场文书
MySql开发之自动同步表结构
2021/05/28 MySQL
MySQL中order by的使用详情
2021/11/17 MySQL