Python 高级编程之初识装饰器

1,390 阅读9分钟

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**kwargsargs 是一个元组,包含所有以值的形式传递的参数,例如 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应用授权、函数参数合法性验证等等,合理利用装饰器可以让我们的代码更加清晰、易于维护。