热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅析Python编写函数装饰器

这篇文章主要介绍了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)

测试如下:

&#63;
1
2
3
4
5
>>> 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..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常见问题]
    在命令行模式下敲命令python,就看到类似如下的一堆文本输出,然后就进入到Python交互模式,它的提示符是>>>,此时我们可以使用print() ... [详细]
  • 深入理解Tornado模板系统
    本文详细介绍了Tornado框架中模板系统的使用方法。Tornado自带的轻量级、高效且灵活的模板语言位于tornado.template模块,支持嵌入Python代码片段,帮助开发者快速构建动态网页。 ... [详细]
  • 在哈佛大学商学院举行的Cyberposium大会上,专家们深入探讨了开源软件的崛起及其对企业市场的影响。会议指出,开源软件不仅为企业提供了新的增长机会,还促进了软件质量的提升和创新。 ... [详细]
  • 深入理解C++中的KMP算法:高效字符串匹配的利器
    本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
  • 探讨一个显示数字的故障计算器,它支持两种操作:将当前数字乘以2或减去1。本文将详细介绍如何用最少的操作次数将初始值X转换为目标值Y。 ... [详细]
  • 在Ubuntu 16.04 LTS上配置Qt Creator开发环境
    本文详细介绍了如何在Ubuntu 16.04 LTS系统中安装和配置Qt Creator,涵盖了从下载到安装的全过程,并提供了常见问题的解决方案。 ... [详细]
  • Python自动化处理:从Word文档提取内容并生成带水印的PDF
    本文介绍如何利用Python实现从特定网站下载Word文档,去除水印并添加自定义水印,最终将文档转换为PDF格式。该方法适用于批量处理和自动化需求。 ... [详细]
  • 本文详细解析了如何使用Python语言在STM32硬件平台上实现高效的编程和快速的应用开发。通过具体的代码示例,展示了Python简洁而强大的特性。 ... [详细]
  • 本文详细解析了Python中的os和sys模块,介绍了它们的功能、常用方法及其在实际编程中的应用。 ... [详细]
  • 如何在PostgreSQL中查看数据表
    本文将指导您使用pgAdmin工具连接到PostgreSQL数据库,并展示如何浏览和查找其中的数据表。通过简单的步骤,您可以轻松访问所需的表结构和数据。 ... [详细]
  • 离线环境下的Python及其第三方库安装指南
    在项目开发中,有时会遇到电脑只能连接内网或完全无法联网的情况。本文将详细介绍如何在这种环境下安装Python及其所需的第三方库,确保开发工作的顺利进行。 ... [详细]
  • 掌握远程执行Linux脚本和命令的技巧
    本文将详细介绍如何利用Python的Paramiko库实现远程执行Linux脚本和命令,帮助读者快速掌握这一实用技能。通过具体的示例和详尽的解释,让初学者也能轻松上手。 ... [详细]
  • 本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ... [详细]
  • 本文介绍如何使用 Python 提取和替换 .docx 文件中的图片。.docx 文件本质上是压缩文件,通过解压可以访问其中的图片资源。此外,我们还将探讨使用第三方库 docx 的方法来简化这一过程。 ... [详细]
  • 自学编程与计算机专业背景者的差异分析
    本文探讨了自学编程者和计算机专业毕业生在技能、知识结构及职业发展上的不同之处,结合实际案例分析两者的优势与劣势。 ... [详细]
author-avatar
Li_pengwei
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有