周希 阅读(37) 评论(0)

装饰器(Decorators)

装饰器是 Python 的一个重要部分。它是修改其他函数的功能的函数,有助于让我们的代码更简短

 

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

 

概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能

为什么需要装饰器

我们假设你的程序实现了func_enter()func_quit()两个函数。

def func_enter():
print "enter!"


def func_quit():
print "enter!" # bug here


if __name__ == '__main__':
func_enter()
func_quit()

运行结果:

enter!
enter!
(wda_python) bash-3.2$ 

但是在实际调用中, 我们发现程序出错了, 上面打印了2个enter。经过调试我们发现是func_quit()出错了

现在假如要求调用每个方法前都要记录进入函数的名称, 比如这样:

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!

一种最直白简单的方式是这样写:

def func_enter():
    print "[DEBUG]: enter func_enter()"
    print "enter!"


def func_quit():
    print "[DEBUG]: enter func_quit()"
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

但是很low对吧, 我们可以试着这样写:

def debug():
    import inspect
    caller_name = inspect.stack()[1][3]
    print '[BEBUG]: enter {}()'.format(caller_name)

def func_enter():
    debug()
    print "enter!"


def func_quit():
    debug()
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

看起来会好一点, 但是每个函数都要调用一次debug()函数,还是不太够, 万一如果又改需求进出不打印调用者了, 其他地方或者函数在打印, 又要大改

怎么办呢? 这个时候装饰器就可以派上用场了

 

怎么写一个装饰器

我们来看一个例子

def debug(func):
    def wrapper():
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func()
    return wrapper

@debug
def func_enter():
    print "enter!"

@debug
def func_quit():
    print "enter!"  # bug here


if __name__ == '__main__':
    func_enter()
    func_quit()

运行结果:

[DEBUG]: enter func_enter()
enter!
[DEBUG]: enter func_quit()
enter!
(wda_python) bash-3.2$ 

这是一个最简单的装饰器, 但是有个问题, 如果被装饰的函数需要传入参数, 那么这个装饰器就坏了,因为返回的函数并不能接受参数

这里可以指定装饰器函数wrapper接受和原函数一样的参数, 比如:

#coding: utf-8

def debug(func):
    def wrapper(something):     # 这里指定一样的参数
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(something)
    return wrapper # 返回包装过的函数

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!

这样解决了传参数的问题, 但是这里有个很大的问题是这里只适配了我们的func_enter和func_quit函数的参数, 如果要用来去装饰其他带参数的函数呢?

还好python提供可变参数*args和关键字参数**kwargs, 有这两个参数装饰器就可以用于任意目标函数了

#coding: utf-8

def debug(func):
    def wrapper(*args, **kwargs):     # 这里指定一样的参数
        print '[DEBUG]: enter {}()'.format(func.__name__)
        return func(*args, **kwargs)
    return wrapper # 返回包装过的函数

@debug
def func_enter(something):
    print "enter {}!".format(something)

@debug
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[DEBUG]: enter func_enter()
enter enter_func!
[DEBUG]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

带参数的装饰器

如果前面我们的装饰器需要完成的功能不仅仅是能在进入某个函数后打印出调用信息,还要指定log级别, 那么装饰器可以是这样:

#coding: utf-8

def debug(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print '[{level}]: enter {func}()'.format(level=level,func=func.__name__)
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@debug(level='Debug')
def func_enter(something):
    print "enter {}!".format(something)

@debug(level='Debug')
def func_quit(something):
    print "enter {}!".format(something)  # bug here


if __name__ == '__main__':
    func_enter("enter_func")
    func_quit("quit_func")

运行结果:

[Debug]: enter func_enter()
enter enter_func!
[Debug]: enter func_quit()
enter quit_func!
(wda_python) bash-3.2$ 

 

基于类实现的装饰器

装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

class Test():
    def __call__(self, *args, **kwargs):
        print 'call me!'

t = Test()
t()

运行结果:

call me!
(wda_python) bash-3.2$ 

__call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

 

回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象(不太严谨,详见后文)。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。

class Debug_info(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print "[DEBUG]: enter function {func}()".format(func=self.func.__name__)
        return self.func(*args, **kwargs)

@Debug_info
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

运行结果:

[DEBUG]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

带参数的类装饰器

如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接受一个函数并返回一个函数。

#coding: utf-8

class Debug_info(object):
    def __init__(self, level='INFO'):
        self.level= level

    def __call__(self, func):  # 接受函数
        def wrapper(*args, **kwargs):
            print "[{level}]: enter function {func}()".format(level=self.level,func=func.__name__)
            func(*args, **kwargs)
        return wrapper

@Debug_info(level='INFO')
def func_enter(something):
    print 'enter {}!'.format(something)

if __name__ == '__main__':
    func_enter("enter_func")

运行结果:

[INFO]: enter function func_enter()
enter enter_func!
(wda_python) bash-3.2$ 

 

内置的装饰器

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改:

s = Student()
s.score = 9999

这显然不合逻辑。为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数:

class Student(object):

    def get_score(self):
        return self._score

    def set_score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

现在,对任意的Student实例进行操作,就不能随心所欲地设置score了:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

但是,上面的调用方法又略显复杂,没有直接用属性这么直接简单。

有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?对于追求完美的Python程序员来说,这是必须要做到的!

还记得装饰器(decorator)可以给函数动态加上功能吗?对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的:

 

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:

>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

 

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

 

装饰器里的那些坑

装饰器可以让你代码更加优雅,减少重复,但也不全是优点,也会带来一些问题。

位置错误的代码

让我们直接看示例代码:

def html_tags(tag_name):
    print 'begin outer function.'
    def wrapper_(func):
        print "begin of inner wrapper function."
        def wrapper(*args, **kwargs):
            content = func(*args, **kwargs)
            print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content)
        print 'end of inner wrapper function.'
        return wrapper
    print 'end of outer function'
    return wrapper_

@html_tags('b')
def hello(name='Toby'):
    return 'Hello {}!'.format(name)

hello()
hello()

在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Toby!</b>
<b>Hello Toby!</b>
(wda_python) bash-3.2$ 

 

错误的函数签名和文档

装饰器装饰过的函数看上去名字没变,其实已经变了。

import datetime

def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
return wrapper

@logging
def say(something):
"""say something"""
print "say {}!".format(something)

print say.__name__
print say.__doc__

运行结果:

wrapper
print log before a function.
(wda_python) bash-3.2$ 

 

为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法。

say = logging(say)

logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给saysay__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如docsource等等。

使用标准库里的functools.wraps,可以基本解决这个问题。

import datetime
from functools import wraps

def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

运行结果:

say
say something
(wda_python) bash-3.2$ 

但是其实还不太完美, 因为函数的签名和源码还是拿不到

import datetime
from functools import wraps
def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logging
def say(something):
    """say something"""
    print "say {}!".format(something)

print say.__name__
print say.__doc__

import inspect
print inspect.getargspec(say)
print inspect.getsource(say)

运行结果:

say
say something
ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
        return func(*args, **kwargs)

(wda_python) bash-3.2$ 

如果要彻底解决这个问题可以借用第三方包,比如wrapt, 后文有介绍

 

不能装饰@staticmethod 或者 @classmethod

当你想把装饰器用在一个静态方法或者类方法时,不好意思,报错了。

class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 装饰实例方法,OK
    def run(self):
        print "{} is running!".format(self.model)

    @logging  # 装饰静态方法,Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print "The model of your car is {}".format(obj.model)
        else:
            print "{} is not a car!".format(obj)

"""
Traceback (most recent call last):
...
  File "example_4.py", line 10, in logging
    @wraps(func)
  File "C:\Python27\lib\functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'staticmethod' object has no attribute '__module__'
"""

前面已经解释了@staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的。

class Car(object):
    def __init__(self, model):
        self.model = model

    @staticmethod
    @logging  # 在@staticmethod之前装饰,OK
    def check_model_for(obj):
        pass

 

如何优化你的装饰器

嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。

decorator.py

decorator.py是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。

from decorator import decorate

def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

def logging(func):
    return decorate(func, wrapper)  # 用wrapper装饰func

你也可以使用它自带的@decorator装饰器来完成你的装饰器。

from decorator import decorator

@decorator
def logging(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)

decorator.py实现的装饰器能完整保留原函数的namedocargs,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)

 

wrapt

wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。

import wrapt

# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print "[DEBUG]: enter {}()".format(wrapped.__name__)
    return wrapped(*args, **kwargs)

@logging
def say(something): pass

使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,argskwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

如果你需要使用wrapt写一个带参数的装饰器,可以这样写:

def logging(level):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print "[{}]: enter {}()".format(level, wrapped.__name__)
        return wrapped(*args, **kwargs)
    return wrapper

@logging(level="INFO")
def do(work): pass

关于wrapt的使用,建议查阅官方文档,在此不在赘述。

  • http://wrapt.readthedocs.io/en/latest/quick-start.html