python装饰器这个东西用得好能给代码结构带来很大的方便,之前一直是用到就翻翻资料,今天做一个总结。

在python中任何东西都是一个对象,包括function。这是装饰器的基础。因为function是一个对象,所以我们可以把一个function当作参数,或者把function当作返回值。于是我们可以在一个function中返回另一个function,借此实现一些特别的功能。比如下面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def printA():
    print 'a'

def printFuncName(func):
    def wrapper():
        print 'func {} called!'.format(func.__name__)
        func()

    return wrapper

>>>printA()
a
>>>printA = printFuncName(printA)
>>>printA()
func printA called!
a

这就实现了在调用printA之前打印日志的功能。当然功能不仅限于打印日志,通过这种方式,我们给printA附加了一些原本不属于它的一些功能,这就叫“装饰”。而python给这种写法提供了一个语法糖(快捷调用方式)“@”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def printFuncName(func):
    def wrapper():
        print 'func {} called!'.format(func.__name__)
        func()

    return wrapper

@printFuncName
def printA():
    print 'a'

>>>printA()
func printA called!
a

使用@printFuncName跟printA = printFuncName(printA)的效果是一样的。以上就是装饰器的一个简单例子,这里例举的printA是一个没有参数的方法,如果是一个有参数的方法,又该怎么写呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def printFuncName(func):
    def wrapper(*args, **xargs):
        print 'func {} called!'.format(func.__name__)
        func(*args, **xargs)

    return wrapper

@printFuncName
def printA(value):
    print value

>>>printA('a')
func printA called!
a

给wrapper加上args和xargs就行了,因为在Python执行到@printFuncName这一行的时候返回的时机是wrapper方法。 到这里,就会有一个问题:由于使用了装饰器,那么实际的printA指向的方法对象已经由原来的方法变成了wrapper,功能是都一样,但是方法的name和document变了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def printFuncName(func):
    def wrapper(*args, **xargs):
        """wrapper"""
        print 'func {} called!'.format(func.__name__)
        func(*args, **xargs)

    return wrapper

@printFuncName
def printA(value):
    """printA"""
    print value

>>> printA.__doc__
'wrapper'
>>> printA.__name__
'wrapper'

要解决这个问题,需要用到functools的wraps方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from functools import wraps
def printFuncName(func):
    @wraps(func)
    def wrapper(*args, **xargs):
        """wrapper"""
        print 'func {} called!'.format(func.__name__)
        func(*args, **xargs)

    return wrapper

@printFuncName
def printA(value):
    """printA"""
    print value

>>> printA.__doc__
'printA'
>>> printA.__name__
'printA'

functools.wraps帮我们将printA原始的name和document复制给了wrapper,解决了上面的问题,但其实还有一个问题,只是这个问题对一般功能的实现不会有影响,但对于需要获取一个被”装饰”的方法的参数时,就会出现问题,这就需要decorator这个python库来解决,我们最后说这个问题。

接下来我们对上面的装饰器再进一步:在装饰器上增加参数。 例如我们在日志时需要用一个参数”level”来标志日志等级,这又如何实现呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from functools import wraps
def printFunc(level='name'):
    def _decorate(func):
        @wraps(func)
        def wrapper(*args, **xargs):
            """wrapper"""
            if level=='name':
                print 'func {} called!'.format(func.__name__)
            else:
                print 'func called!'
            
            func(*args, **xargs)

        return wrapper
    
    return _decorate

@printFunc(leval='args')
def printA(value):
    """printA"""
    print value

>>> printA('b')
func called!
b
>>> printA.__name__
'printA'
>>> printA.__doc__
'printA'

就是在原来的装饰器方法外面再套一层,这一层返回的其实是一个装饰器,在执行到@printFunc(level=‘args’)时最外层方法被执行,返回一个装饰器,至于在wrapper里用到level参数则是利用了python闭包的特性。

除了一个方法可以作为一个装饰器之外,类也可以作为装饰器,我们用类作为装饰器来改写一些上面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import wraps
class printFuncName(object):
    def __init__(self):
        pass
    def __call__(self, func)
        @wraps(func)
        def wrapper(*args, **xargs):
            """wrapper"""
            print 'func {} called!'.format(func.__name__)
            func(*args, **xargs)

        return wrapper

@printFuncName()
def printA(value):
    """printA"""
    print value

>>> printA('A')
func printA called!
A
>>> printA.__name__
'printA'
>>> printA.__doc__
'printA'

用类作为装饰器就是利用类的call方法,来返回一个wrapper。上面的代码是一个不带参数的装饰器,一个带参数的装饰器这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from functools import wraps
class printFuncName(object):
    def __init__(self, level='name'):
        self.level = level
    def __call__(self, func)
        @wraps(func)
        def wrapper(*args, **xargs):
            """wrapper"""
            if self.level == 'name':
                print 'func {} called!'.format(func.__name__)
            else:
                print 'func called!'
            func(*args, **xargs)

        return wrapper

@printFuncName()
def printA(value):
    """printA"""
    print value

>>> printA('A')
func printA called!
A
>>> printA.__name__
'printA'
>>> printA.__doc__
'printA'

就是在类的int方法中接收装饰器参数,然后再call方法中使用。

最后再说之前遗留的一个问题:尽管我们用了functools.wraps后经过装饰器装饰的方法与原方法再name和document上都与原来一样了,但是它的参数列表还是不一样的,比如下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from functools import wraps
def printFuncName(func):
    @wraps(func)
    def wrapper(*args, **xargs):
        """wrapper"""
        print 'func {} called!'.format(func.__name__)
        func(*args, **xargs)

    return wrapper

def printA(value):
    """printA"""
    print value

>>> inspect.getargspec(printA)
ArgSpec(args=['value'], varargs=None, keywords=None, defaults=None)

@printFuncName
def printA(value):
    """printA"""
    print value
>>> printA.__doc__
'printA'
>>> printA.__name__
'printA'
>>> getargspec(printA)
ArgSpec(args=[], varargs='args', keywords='xargs', defaults=None)

可以看到,这里被装饰器装饰过后,方法的参数列表发生了变化。在需要用到方法的参数列表做一些判断的时候,这里就要小心了;如果一个方法是经过装饰器装饰过的,那么你取到的参数列表可能有问题。那么如何解决呢?这就需要用到python的decorator库

接着来看decorator库。decorate库主要是为了解决在使用装饰器的过程中,方法签名发生变化的问题。一个很典型的例子是在odoo的openerp/api.py中,许多装饰器例如@api.one, @api.model, @api.multi等都会先获取一些被装饰方法的参数列表,以此来做一层api转换。

最近在做一些性能优化的工作,要加一段profile装饰器的代码,来给需要的方法做性能评估。比较好的方式是加到openerp/api.py中,这时就遇到上面说的问题,即如何在使用装饰器的时候保证被装饰方法的签名,这样在使用多个装饰器时不受影响。

直接看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from decorator import decorator
from pyinstrument import Profiler
from cProfile import Profile
def do_profile(profiler_type='pyinstrument', save=False):
    def _decorate(func):
            def wrapper(func, *args, **xargs):
                if profiler_type == 'pyinstrument':
                    profiler = Profiler()
                    profiler.start()
                elif profiler_type == 'cprofile':
                    profiler = Profile()
                    profiler.enable()
                res = func(*args, **xargs)
                if profiler_type == 'pyinstrument':
                    profiler.stop()
                    print(profiler.output_text(unicode=True, color=True))

                elif profiler_type == 'cprofile':
                    profiler.disable()
                    profiler.print_stats(sort=2)

                return res

            return decorator(wrapper, func)

        return _decorate

这里用到带参数的装饰器,可以在使用的时候选择profiler。顺带提一下,cProfile对程序执行的效率影响比较大,pyinstrument这个profiler对程序执行影响较小,其结果展示也比较直观。

最核心的就是用到了decorator.decorator。它提供了一种非常简单的方式,将一个装饰器包装为一个保留被装饰方法签名的装饰器。一般在_decorate方法返回时,会直接return wrapper,这时这个wrapper与被装饰方法func相比,就丢失了func原本的方法签名。这是使用decorator(wrapper, func)就能轻松解决。注意,wrapper方法的定义需要做微调,即第一个参数为被装饰的func。