装饰器(decorator)是一种软件设计模式,装饰器可以在不改变原函数的代码的情况下改变原函数的功能。本文对Python装饰器的基本用法作一扼要说明。

一个简单的例子

下面的一个函数返回两个数相加后的结果:

def add(x, y):
    '''
    add two numbers
    '''
    return x + y

例如:

>>> z = add(-8, 9)
>>> print(z)
1

现在我想把输入的数字先转化成绝对值,然后再相加,该怎么办?很简单,直接更改add(x, y)函数的代码为:

def add(x, y):
    '''
    add two numbers
    '''
    if x < 0:
       x = -x
    if y < 0:
       y = -y
    return x + y

但是如果我不想改动原来的函数呢?这时候就要用到装饰器了。我们先写下如下的装饰器代码:

def abs_add(func):
    def make_change(x, y):
        if x < 0:
            x = -x
        if y < 0:
            y = -y
        return func(x, y)
    return make_change

从上面的代码可以看出装饰器abs_add(func)其实也是一种函数,只不过看起来稍微奇怪点。从整体上来看,它接收一个函数参数,这个函数参数就是我们要对其进行‘装饰’的函数,‘装饰’被输入的函数也就是对这个函数进行功能上的改造,最后返回一个函数make_change(x, y)函数,这个函数也就是改造之后的函数了。我们现在利用abs_add(func)装饰器来对原有的add(x, y)函数进行改造:

>>> add = abs_add(add)
>>> z = add(-8, 9)
>>> print(z)
17

可见我们利用装饰器实现了我们想要改变的功能,却并没有改动原函数的代码,这就是装饰器最基本最简单的用途。另外说明一点,使用装饰器一般不采用add = abs_add(add)这种写法,我们一般用如下的方式来使用,这种写法完全与add = abs_add(add)等价:

@abs_add
def add(x, y):
    '''
    add two numbers
    '''
    return x + y

函数名称等属性的变化

一个函数在经过装饰器修饰之后,它的名字会发生变化。举例来说,如下函数是实现把一个数字乘以2倍并返回的:

def double(x):
    '''Double a number.'''
    return 2 * x

现在再写一个简单的装饰器,该装饰器不对原函数作任何修改:

def decorator(func):
    def you_will_see_this_name(*args, **kwargs):
        '''Haha! your name has been changed, you_will_see_this_name!'''
        return func(*args, **kwargs)
    return you_will_see_this_name

我们运行:

@decorator
def double(x):
    '''Double a number.'''
    return 2 * x
    
print(double(2))    

输出结果是:

4

看起来没有对原函数做了些什么,原函数仍旧输出数字的二倍,但是我们要说的不是这个,而是,double(x)函数的名字已经不是double了:

>>> double.__name__
'you_will_see_this_name'

可见已经变成装饰器里面you_will_see_this_name(*args, **kwargs)函数的名字了!再看函数的文档说明,原double(x)函数的说明是'Double a number.',但是我们看到:

>>> double.__doc__
'Haha! your name has been changed, you_will_see_this_name!'

可见文档说明也被改变了,函数的这些属性的改变可能会对函数接下来的使用造成一定的麻烦,我们能否保持函数的这些属性不变呢?是可以的。我们可以写一个‘装饰器的装饰器’,对已有的装饰器进行功能上的改造,把原来函数的一些属性都复制到新的函数上面去:

def decorator_of_decorator(decorator):
    def new_decorator(f):
        g = decorator(f)
        g.__name__ = f.__name__
        g.__doc__ = f.__doc__
        g.__dict__.update(f.__dict__)
        return g
    return new_decorator

现在我们再重复上面的代码,只不过在装饰器头顶上添加了一个@decorator_of_decorator:

@decorator_of_decorator
def decorator(func):
    def you_will_see_this_name(*args, **kwargs):
        '''Haha! your name has been changed, you_will_see_this_name!'''
        return func(*args, **kwargs)
    return you_will_see_this_name

@decorator
def double(x):
    '''Double a number.'''
    return 2 * x

现在我们就会发现函数的属性已经不再发生变化了

>>> double.__name__
'double'
>>> double.__doc__
'Double a number.'

可是新的问题来了,你对已有的装饰器进行了改造,那么被改造的装饰器不也遭遇了被改造的函数一样的问题了么:属性变了。例如我们来看:

>>> decorator.__name__
'new_decorator'

可见装饰器decorator(func)的名字已经变成了新的名字,同样地,我们如何避免呢?只需在decorator_of_decorator(decorator)中添加几行代码即可:

def decorator_of_decorator(decorator):
    def new_decorator(f):
        g = decorator(f)
        g.__name__ = f.__name__
        g.__doc__ = f.__doc__
        g.__dict__.update(f.__dict__)
        return g
    new_decorator.__name__ = decorator.__name__
    new_decorator.__doc__ = decorator.__doc__
    new_decorator.__dict__.update(decorator.__dict__)
    return new_decorator

现在我们要是重复以上过程,就会发现不论是被装饰的函数还是被装饰的装饰器,它们的属性都不会被改变了。但是实际应用中真的要这么麻烦么?实际上已经有现成的functools库可以帮助我们完成上述任务,为实现函数属性不被改变的目的,只需在原装饰器里添加一行代码即可:

import functools

def decorator(func):
    @functools.wraps(func)
    def you_will_see_this_name(*args, **kwargs):
        '''Haha! your name has been changed, you_will_see_this_name!'''
        return func(*args, **kwargs)
    return you_will_see_this_name

@decorator
def double(x):
    '''Double a number.'''
    return 2 * x

接下来double(x)函数的属性就不会发生变化了。

带参数的装饰器

如果需要对装饰器传入参数,那么就需要定义一个返回装饰器的高阶装饰器,类似于我们在上一节已经见过的‘装饰器的装饰器’。举例来说,倘若你想把一个函数重复执行若干次,怎么用装饰器来实现呢?比如你想把下面的hello()函数重复执行多次:

def hello():
    '''print hello world'''
    print('Hello world!')

可以编写的装饰器如下所示:

import functools

def run_times(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            '''repeat func several times'''
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

其中times为函数要重复执行的次数,run_times(times)装饰器就是一个‘装饰器的装饰器’,因为它对装饰器进行修饰然后返回。运行如下代码我们就可以把hello()函数重复执行三次:

@run_times(3)
def hello():
    '''print hello world'''
    print('Hello world!')

结果如下:

Hello world!
Hello world!
Hello world!

以上就是对装饰器作的简单介绍。另附几个文档和博客,本文部分内容是参考它们的。

posted on 2018-05-26 16:16  艾克_塞伦特  阅读(162)  评论(0编辑  收藏  举报