1. 什么是装饰器
装饰器是一种修改函数和类的便捷方式,是Python提供的语法糖。它可以是我们定义的函数(函数装饰器),也可以是一个类(类装饰器)。我们可以使用装饰器快速地修改其它函数或类,而不用改变它们原本的代码,这样一来,我们就可以方便地统一管理某一类函数或者类。
在接下来的文章中,我们主要探讨函数装饰器。
例如,这有一个简单的函数:
def hello():
print 'hello'
我们想要在函数每次执行时都打印一条记录,一种方法是直接修改函数内部的代码:
def hello():
print 'function %s called' % hello.__name__
print 'hello'
如果我们想要记录多个函数的执行状况,这种方法就需要对多个函数的内部代码进行修改,麻烦且不容易维护!另一种方法就是使用装饰器:
@record
def hello():
print 'hello'
是不是非常方便简洁?在这里,我们使用了一个自己定义的名叫 record
的装饰器。实际上Python做的等价于以下这些事情:
def hello():
print 'hello'
hello = record(hello)
装饰器自动帮我们修改位于它之后的函数。当我们要修改的函数有很多个时,更能感受到装饰器的便捷之处,只需要在每个函数前加上 @record
即可。
我们下面就来看看怎么实现 record
这个装饰器。
2. 装饰器的实现
装饰器的实现原理其实很好理解,就是以待修改的函数作为参数,经过封装后返回修改后的函数。 在上面给出的例子中,我们在函数执行时打印出一条记录,我们可以写一个 record
装饰器来实现这一功能。
def record(func):
def wrapper():
print 'function %s called' % func.__name__
func()
return wrapper
我们来看一下这段代码的意思:首先我们定义了一个装饰器函数 record
,它接收一个参数,就是待修改的函数。在函数内部,我们又定义了一个函数 wrapper
,用于替换原函数的功能并作为返回值。 所以,我们把要实现的功能全都写到了 wrapper
函数里。在这里,就是先打印一条记录,再执行原函数。
接下来就用这个装饰器来修改几个函数:
>>> @record
... def hi():
... print 'hi'
>>> @record
... def hey():
... print 'hey'
>>> hi()
function hi called
hi
>>> hey()
function hey called
hey
瞧!我们实现了一个最简单的装饰器。不知道你注意到没有,在上面的例子中,我们修改的函数都是不带参数的。如果直接用上面的 record
装饰器对带参数的函数进行修改,就会引发错误!
>>> @record
... def hello(name):
... print 'hello', name
>>> hello('Wray')
Traceback (most recent call last):
File "<interactive input>", line 1, in <module>
TypeError: wrapper() takes no arguments (1 given)
仔细观察 TypeError
所报的错误,它提示 wrapper
函数不接收任何参数,而我们却传进了一个参数。
我们调用的明明是 hello
函数,为什么这里会显示 wrapper
函数?别忘了,我们之前是在 wrapper
函数里定义好修改后的函数行为,并且最后返回的函数也是 wrapper
。因此,现在调用的 hello
函数实际上是装饰器返回的 wrapper
函数。
hello
函数被替换为 wrapper
函数后,产生了两个问题:一个是原函数的信息丢失,另一个是参数在传递过程中丢失。我们将在下面的文章中探讨如何解决这两个问题。
为了简单起见,我先以不带参数的函数作为例子,参数传递的问题留到后面解决。接下来,先看看如何避免原函数的信息被修改,因为这会对其它地方的函数调用产生影响。
3. 保留原函数信息
从上文的分析可以知道,hello
函数就是 wrapper
函数。所以,我们只要在装饰器函数返回 wrapper
函数前,将其信息替换为原函数的信息即可。
def record(func):
def wrapper():
print 'function %s called' % func.__name__
func()
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
#Other attributes of func to replace
return wrapper
现在,我们的装饰器已经能够保留原函数的基本信息了。不过你有没有发现,我们修改 wrapper
函数的行为跟最开始介绍装饰器时修改 hello
函数其实是一样的。那为什么不用装饰器来完成这一步呢?
def record(func):
@myWraps(func)
def wrapper():
print 'function %s called' % func.__name__
func()
return wrapper
比起之前的代码是不是好多了!不过,你可能注意到了这里的装饰器用法 @myWraps(func)
跟之前不同,@
后面并不只是函数名,而是函数调用。
那就让我们来分析一下,之前使用的形式是 @record
,@
后面跟的是一个装饰器函数。那么 myWraps(func)
的返回值就应该是个装饰器函数。
下面来我们就来实现 myWraps
这个给装饰器使用的装饰器。代码不太容易理解,你也可以选择跳过这段代码,因为实际上我们并不用自己实现它,只不过这可以加深我们对装饰器的理解。
def myWraps(func):
def decorator(wrapper):
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
return decorator
希望你没有被绕晕。myWraps
被调用后返回的 decorator
才是我们想要的装饰器函数。这个装饰器是专门修改其它装饰器中的 wrapper
函数的,我们在这里只是简单地把 wrapper
函数的信息修改为原函数 func
的信息,返回的还是 wrapper
函数,因此就不用在内部定义一个用于返回的函数了。
Python为我们提供了一个工具,它的功能比我们上面自己实现的 myWraps
要完善得多。我们可以从 functools
中导入 wraps
函数。
from functools import wraps
在装饰器函数中使用它:
def record(func):
@wraps(func)
def wrapper():
print 'function %s called' % func.__name__
func()
return wrapper
这样,原函数信息丢失的问题就解决了!
4. 传递原函数参数
你是否还记得,我们之前调用 hello
函数时,Python提示的错误信息中显示的是 wrapper
函数?这正是我们刚解决的原函数信息丢失的问题。不过这也告诉我们,参数传递的问题出在了 wrapper
函数上面。
因此,我们需要修改 wrapper
函数,使其正确传递原函数的参数。
下面是修改后的 record
装饰器:
from functools import wraps
def record(func):
@wraps(func)
def wrapper(*args, **kwargs):
print 'function %s called' % func.__name__
func(*args, **kwargs)
return wrapper
这里我们用到了两个特殊的参数 *args
和 **kwargs
。args
是一个元组,包含所有以值的形式传递的参数,例如 f(1,2)
。而 kwargs
是一个字典,包含所有以关键字的形式传递的参数,例如 f(a=1, b=2)
。
我们通过 *args
和 **kwargs
来获取外界传给 wrapper
的参数,而不用关心具体是什么。*
将元组 args
展开成形如 arg1, arg2, ...
的参数表,传递给 func
。而 **
将字典 kwargs
展开成形如
k1=arg1, k2=arg2
的关键字参数表,传递给 func
。这样,我们就能确保所有类型的参数都被 wrapper
函数接收,再传递给 func
函数执行。
Tips: 运算符 *
不仅可以展开元组,还可以展开其它可迭代对象,例如列表、集合、字典等等。
5. 保留原函数返回值
到目前为止,我们已经解决了装饰器的原函数信息保留、函数参数传递这两个棘手的问题,现在只要再加上返回值就大功告成了。
我们继续完善 record
装饰器:
from functools import wraps
def record(func):
@wraps(func)
def wrapper(*args, **kwargs):
print 'function %s called' % func.__name__
result = func(*args, **kwargs)
return result
return wrapper
OK,经过最后完善的 record
装饰器已经可以正式投入使用了!
6. 装饰器的应用
6.1. 注册函数
在以事件模型为处理机制的程序中,经常需要将函数注册到一个API中,以便对这些函数进行统一的管理。
我们可以使用装饰器方便地实现这一功能:
registry = {}
def register(func):
registry[func.__name__] = func
return func
在需要注册的函数前使用该装饰器:
@register
def f1():
pass
@register
def f2():
pass
已注册的函数:
>>> registry
{'f1': <function f1 at 0x02F279B0>, 'f2': <function f2 at 0x02F27930>}
6.2. 扩展函数
我们可以使用装饰器对函数进行扩展。例如,我们可以对函数的信息进行扩展:
def add_info(func):
func.extraInfo = 'extra information'
return func
在这里,我们只对原函数添加了一个属性,返回的还是原函数,因此函数的信息不会丢失,无需使用 wraps
装饰器。
>>> @add_info
... def hello(name):
... print 'hello', name
>>> hello.extraInfo
'extra information'
6.3. 日志记录
日志记录是生产环境中的重要步骤,就像前面介绍的 record
函数一样,我们可以使用装饰器来实现日志的记录。在这里我们给出之前介绍的例子,在具体使用中,可以根据自己的需求进行修改。
from functools import wraps
def record(func):
@wraps(func)
def wrapper(*args, **kwargs):
print 'function %s called' % func.__name__
result = func(*args, **kwargs)
return result
return wrapper
6.4. 计时器
在一些需要对性能进行评估的地方,常常需要对某个函数的执行过程进行计时。我们可以编写一个通用的计时器,可用于任何函数的计时。
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.clock()
result = func(*args, **kwargs)
elapsed = time.clock() - start
print '%s executed with %.5f seconds' % \
(func.__name__, elapsed)
return result
return wrapper
我们写两个函数进行测试:
>>> @timer
... def f1(max):
... return [x ** 2 for x in range(max)]
>>> @timer
... def f2(max):
... return map((lambda x: x ** 2), range(max))
>>> f1(100000)
f1 executed with 0.07050 seconds
>>> f2(100000)
f2 executed with 0.07960 seconds
我们清楚地看到了两个函数的执行过程所用时间。从这个结果也能看出,列表推导式的效率要高于 map
函数。
装饰器还有许多其它的应用,例如web应用授权、函数参数合法性验证等等,合理利用装饰器可以让我们的代码更加清晰、易于维护。