Python中的装饰器介绍

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

闭包

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

1. 隐藏你要使用的对象,只有返回的内部函数才能访问它。这一点,在Javascript闭包一文中已经提过。
2. 将拥有类似功能的函数统一实现,差异部分由传入的参数来区别,并通过内部函数来返回我们真正要用的函数。

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

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

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

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

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

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

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

装饰器

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

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

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

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

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

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

这个例子就等同于:

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

带参数的装饰器

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

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

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

保留函数名

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

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

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

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

类装饰器

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

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

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

装饰器返回的还是原来的类,只是已经把它所有的用户定义的成员函数都用”add_log”替换掉了。我们通过定义两个类来试验下,”ClassA”有一个成员函数”test1″,”ClassB”继承”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装饰器,动手写写吧。

转载请注明出处: 思诚之道

《Python中的装饰器介绍》有9个想法

  1. 感谢楼主,听过两次装饰器的课了,都没太明白,看了楼主的解释,做了一下代码实验。理解的差不多了。
    谢谢。

发表评论

电子邮件地址不会被公开。 必填项已用*标注