装饰器模式Decorator可以动态的扩充一个类或者函数的功能,实现的方法一般是在原有的类或者函数上包裹一层修饰类或修饰函数。在Python语言中,其提供了语法糖,让装饰器使用起来更简便,不过同时也增加了初学者理解这个装饰器背后原理的难度。这里,我们就来剖析下Python的装饰器是个什么东东。

闭包

要理解Python的装饰器,就先要了解闭包。基本上支持函数式编程(函数可以作为对象传递)的语言,都支持闭包。我在之前Javascript闭包中曾介绍过它,Python的实现基本上也一样,就是在函数中返回其内部函数,这样当外部函数的生命周期结束后,其被内部函数使用的资源还会被保存下来。个人觉得,闭包主要有两个作用:

  1. 隐藏你要使用的对象,只有返回的内部函数才能访问它。这一点,在Javascript闭包一文中已经提过。

  2. 将拥有类似功能的函数统一实现,差异部分由传入的参数来区别,并通过内部函数来返回我们真正要用的函数。

上面的第二个作用听上去有点拗口,其实它有个很高大上的名字,叫函数柯里化 (Currying)。我们还是看例子吧,本文的例子都是由python写的,并在Python2.7环境下运行。

def multiply(number):
    def in_func(value):
        return value * number
    return in_func

double = multiply(2)
triple = multiply(3)
print double(5)
print triple(3)

内部函数in_func()的功能就是返回其参数value同外部函数参数number的乘积。所以,当我们想要一个double函数时,就将2传入外部函数,这样返回的内部函数的功能就是将参数乘2;同样,将3传入外部函数后,返回的内部函数就是将参数乘3,也就是triple函数。有了这个闭包,我们就无需为double, triple专门定义函数,只要通过调用multiply()函数返回即可。multiply()函数生命周期结束后,其被内部函数in_func()使用的参数变量number不会被销毁,它会保存在in_func.__closure__中。之后,像double函数想使用它,就会从double.__closure__中找到之前保存的number值。

上例是通过在外部函数传入数值类型的变量,来获取不同的内部函数实现。那我们复杂点,如果传入的是函数对象呢?

import logging
LOG_FILE = "test.log"
logging.basicConfig(filename = LOG_FILE, level = logging.DEBUG)
logger = logging.getLogger(__name__)

def add_log(func):
    def newFunc():
        logger.debug("Before %s() call" % func.__name__)
        func()
        logger.debug("After %s() call" % func.__name__)
    return newFunc

def funcA():
    print "In funcA"

def funcB():
    print "In funcB"

newFuncA = add_log(funcA)
newFuncB = add_log(funcB)
newFuncA()
newFuncB()

我们引入logging包来实现一个日志功能。上例中,funcA()funcB()两个函数只做了屏幕打印功能,而当我们把这两个函数对象传入add_log()函数后,其返回的newFuncAnewFuncB功能上同原来的方法一样,但是在每次调用方法的前后,newFuncAnewFuncB都会自动记一条日志(打开本地”test.log”文件看看,是不是记录了日志)。这样做,我们就无需为每个要记录日志的函数都加上logger.debug代码,只要将其传入add_log(),并返回一个新函数即可。回想下我们刚才讲的闭包的第二个功能,就是将各个函数的通用功能统一实现,不同的功能由传入的参数来区分,是不是这个样子呢?

上面add_log()的例子通用型不好,因为它只能对没有参数及没有返回值的函数加日志,我们将其改的更通用点:

def add_log(func):
    # *args matches all arguments without key
    # **kwargs matches all key=value arguments
    def newFunc(*args, **kwargs):
        logger.debug("Before %s() call" % func.__name__)
        ret = func(*args, **kwargs)
        logger.debug("After %s() call" % func.__name__)
        return ret
    return newFunc

def funcC(x, y):
    print x + y

newFuncC = add_log(funcC)
newFuncC(2, y=3)

简单解释下*args**kwargs,这两个都是匹配函数调用时所有的参数。*args是一个列表,它包含了所有没指定”key”的参数,比如在上例中调用newFuncC(2, y=3)args列表将是[2]。而**kwargs是一个字典,它包含了所有”key=value”类型的参数,比如上例中kwargs将是{'y': 3}。另外,我们还保留的返回值,确保新的newFuncC()函数能返回原本funcC()的返回值。这样,我们的这个能加日志的闭包就对所有类型的函数都通用了。

看了上面的例子,大家有没有觉得很像面向切面的编程AOP啊?是的,其实装饰器模式的很大一部分价值,就是AOP。但是我们讨论了那么多闭包的功能,跟Python的装饰器到底有什么关系呢?别着急,谜底马上揭晓。

装饰器

基于上面最后一个例子,我们来做一个神奇的改动,add_log()方法不变,只在funcA()等函数上加个修饰符。

def add_log(func):
    def newFunc(*args, **kwargs):
        logger.debug("Before %s() call" % func.__name__)
        ret = func(*args, **kwargs)
        logger.debug("After %s() call" % func.__name__)
        return ret
    return newFunc

@add_log
def funcA():
    print "In funcA"

@add_log
def funcC(x, y):
    print x + y

funcA()
funcC(2, y=3)

运行funcAfuncC,你会发现日志居然也被记录了。奇怪,我们根本没有调用add_log()方法生成新的函数呀。那到底是怎么会是呢?答案就是,这个装饰器语法糖@add_log的作用,就等同于:

funcA = add_log(funcA)

也就是加上装饰器后,当前的函数其实就变成了经过闭包调用后生成的新函数了。索迪斯噶~!原来Python装饰器背后的原理就是一个以当前函数为参数的闭包呀,很好理解吧。

从《Python高级编程》书上摘到,常见的装饰器使用场景有:

  • 参数检查
  • 缓存
  • 代理
  • 上下文提供者

装饰器可以定义多个,调用顺序自下而上,也就是离函数定义最近的装饰器先被调用。

def bold(func):
    def newFunc(*args, **kwargs):
        return "<b>" + func() + "</b>"
    return newFunc

def italic(func):
    def newFunc(*args, **kwargs):
        return "<i>" + func() + "</i>"
    return newFunc

@bold
@italic
def foo():
    return "Hello"

print foo()

这个例子就等同于:

foo = bold(italic(funcA))

我们对返回的内容先加斜体,再加粗。运行下,看看结果是不是<b><i>Hello</i></b>

带参数的装饰器

我们来逐步进阶,装饰器上可不可以带参数呢,答案是可以。装饰器可以带多个参数,参数可以是任何类型的变量。让我们扩充下add_log()装饰器的功能:

import time
def add_log(logger, timeFormat="%b %d, %Y - %H:%M:%S"):
    def decorator(func):
        def newFunc(*args, **kwargs):
            logger.debug("Start %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            ret = func(*args, **kwargs)
            logger.debug("Finish %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            return ret
        return newFunc
    return decorator

@add_log(logger)
def funcA():
    print "In funcA"

@add_log(logger, timeFormat="%m-%d-%Y %H:%M:%S")
def funcB():
    print "In funcA"

funcA()
funcB()

例子中引入了time包来获取当前时间。装饰器函数有两个参数,一个是日志对象logger,另一个是时间显示格式timeFormat。大家注意到,这个装饰器函数同之前最大的不同,就是有两层嵌套函数,最外层的函数接受了装饰器参数后,里面两层内部函数其实同上例中无参数的装饰器(也就是闭包函数)一模一样。之后,我们就可以用@add_log(logger)来修饰函数funcA,因为没输入timeFormat所以日期格式会采用默认值。其实这样的声明效果,就等同于:

funcA = add_log(logger)(funcA)

这是一个高阶函数。也就是说,Python先调用add_log(logger)返回一个闭包函数,也就是上例中声明的decorator(func),再调用decorator(funcA)来返回其内部函数newFunc。因为内部函数可以访问外部函数的局部变量,所以这里它可以获取到最外层函数的参数loggertimeFormat。大家可以运行下例子看结果。

保留函数名

这里再补充一个知识点,就是当我们用装饰器修饰函数后,该函数其实已经不是原来的函数了,而是通过装饰器返回的内部函数。所以如果你打印函数名时,会出现装饰器内部函数的名字。比方说,在上面的例子中,我们输出funcA的函数名,结果会是newFunc

print funcA.__name__    # Result is "newFunc"

那我们希望函数名还是原来的怎么办?有朋友马上动手,改动add_log()的代码,在内部函数返回前,将其__name__改了:

...
        def newFunc(*args, **kwargs):
            logger.debug("Start %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            ret = func(*args, **kwargs)
            logger.debug("Finish %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            return ret

        newFunc.__name__ = func.__name__
        return newFunc
...

这样做当然可以,就是每个装饰器都要这样写,有点麻烦。而且,__doc__怎么办,也补上同样的代码吗?Python提供了functools.wraps装饰器,你只需要将其修饰在装饰器返回的内部函数上即可。

from functools import wraps

def add_log(logger, timeFormat="%b %d, %Y - %H:%M:%S"):
    def decorator(func):
        @wraps(func)    # Reserve __name__, __doc__, __module__
        def newFunc(*args, **kwargs):
            logger.debug("Start %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            ret = func(*args, **kwargs)
            logger.debug("Finish %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            return ret
        return newFunc
    return decorator

@add_log(logger)
def funcA():
    '''This is funcA'''
    print "In funcA"

print funcA.__name__
print funcA.__doc__

注意,@wraps需要接受一个参数,即装饰器传入的被修饰的函数本身。运行看下结果,函数名及描述的确保留下来了吧。

类装饰器

上面所说的装饰器都是修饰在函数声明上的,那在类声明上呢?我们一样可以写装饰器。根据函数装饰器的例子,我们可以猜测下,类装饰器要怎么实现呢?首先,它肯定也是要将一个类传入装饰器函数,这样才能对这个类做修改。然后呢,应该也是要返回一个修改后的类。再者,它肯定也能支持参数,也就是可以在装饰器函数外加一层函数来接受参数。基于以上的理解,我们尝试着写一下类装饰器,还是重用前面加日志的代码,将一个类里所有的成员函数都加上日志的功能:

def add_log(logger, timeFormat = "%b %d, %Y - %H:%M:%S"):
    def decorator(func):
        if hasattr(func, "_logger_added") and func._logger_added:
            return func    # Stop add logger again if it's already decorated

        @wraps(func)
        def newFunc(*args, **kwargs):
            logger.debug("Start %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            ret = func(*args, **kwargs)
            logger.debug("Finish %s() call on %s" % (func.__name__, time.strftime(timeFormat)))
            return ret

        newFunc._logger_added = True    # Set attribute to mark the logger is added
        return newFunc
    return decorator

def log_methods(timeFormat = "%b %d, %Y - %H:%M:%S"):
    def decorator(clz):
        logger = logging.getLogger(clz.__name__)
        # Iterate the name of member (methods, attributes) of the class
        for member_name in dir(clz):
            if member_name.startswith("__"):   # Do not log built-in members
                continue
            member = getattr(clz, member_name)      # Get the member
            if hasattr(member, "__call__"):    # Check if member is a function
                decorated_func = add_log(logger, timeFormat)(member)
                # Set the decorated function to class' orginal member
                setattr(clz, member_name, decorated_func)
        return clz
    return decorator

@log_methods()
class ClassA(object):
    def test1(self):
        print "test1"

@log_methods(timeFormat = "%m-%d-%Y %H:%M:%S")
class ClassB(ClassA):
    def test1(self):
        super(ClassB, self).test1()
        print "child test1"
    def test2(self):
        print "test2"

obj1 = ClassA()
obj2 = ClassB()

obj1.test1()
obj2.test1()
obj2.test2()

代码比较长,重要的部分我都加了注释,这里我解释一下:

  1. 之前的函数装饰器add_log()函数基本不变,就是加了一个检查,防止日志功能被重复添加。这样即使你在类的成员函数声明上同时加了装饰器,也不会将日志记录两次。
  2. 类装饰器函数里首先用dir()方法将类中所有成员的名字列出来,并过滤到所有以__开头的内置成员。
  3. 对于用户定义的成员,通过检查其是否存在__call__属性来判断其是否是一个函数。
  4. 对于每个成员函数,调用add_log(logger, timeFormat)(member),来创建一个新函数。新函数会在原来函数的功能上记录日志。
  5. 通过setattr(clz, member_name, decorated_func)将新函数设置到类中,并覆盖掉原来的函数。

装饰器返回的还是原来的类,只是已经把它所有的用户定义的成员函数都用add_log()替换掉了。我们通过定义两个类来试验下,ClassA有一个成员函数test1ClassB继承ClassA,重写了其test1函数,并加上了test2函数。我们实例化两个类对象,并调用一下成员函数。看看你是不是也得到了如下的结果:

DEBUG:ClassA:Start test1() call on Jan 12, 2016 - 23:56:10
DEBUG:ClassA:Finish test1() call on Jan 12, 2016 - 23:56:10
DEBUG:ClassB:Start test1() call on 01-12-2016 23:56:10
DEBUG:ClassA:Start test1() call on Jan 12, 2016 - 23:56:10
DEBUG:ClassA:Finish test1() call on Jan 12, 2016 - 23:56:10
DEBUG:ClassB:Finish test1() call on 01-12-2016 23:56:10
DEBUG:ClassB:Start test2() call on 01-13-2016 23:56:10
DEBUG:ClassB:Finish test2() call on 01-13-2016 23:56:10

类装饰器的实现方法很多,你也可以在其中定义一个代理类,然后完全替代掉原来的类。具体怎么做,就看你的业务逻辑需要了。

总结

文章比较长,快速总结下。我们介绍了Python的装饰器,它可以修饰在函数上,也可以修饰在类上。它的实现原理就是一个闭包,实现的功能类似于AOP。Python提供了@语法糖来让装饰器的使用很有逼格。你可以通过高阶函数来使得装饰器可以接受参数。想要更好的掌握Python装饰器,动手写写吧。