浅析Python装饰器

Decorator(装饰器)是在写Python代码的过程中,经常会被用到的一个语言特性,它可以大幅度减少重复的模板代码,并且,对于已有代码的重构往往也有奇效。但是,实现一个Decorator时的重重嵌套函数定义,经常让人头晕。下面就以一个常见的函数Cache装饰器作为例子,浅析Python中的装饰器特性。

Decorator简介

首先要注意的是,Python在引入Decorator时,其实并没有引入任何新的语言特性,因为Decorator只是一种“语法糖”,不使用@decorator这样的语法,也完全可以使用Python的原有语法实现Decorator的功能。这得益于Python中一切皆是对象。对于这样的一个decorator:

也就相当于:

这种写法就像对原程序打了一个Monkey Patch。

Deocrator基本应用

无参数Decorator

下面用一个缓存函数返回值的Decorator说明其最基本的实现方式:

其中,func_cache就是我们实现的Decorator,它以一个函数对象(func)作为参数,返回另一个函数对象(inner_deco),因此,当我们每次调用被func_cache装饰过的函数(add_two_number)时,调用的其实是inner_deco,也即:

在这里,可以给出Decorator的一个粗浅定义:Decorator是一个函数,它以一个函数对象A为参数,返回另一个函数对象B。对象B定义在Decorator体内,形成一个闭包。函数A和函数B接受的参数相同。每当程序调用函数A时,实际上会转换为对函数B的调用。

再看inner_deco,它内部实现的就是函数返回值缓存的逻辑,并打印了一些调试信息。

但是这里有一个明显的问题:inner_deco只能接受*arg,也就是列表参数,这就限制了这个Decorator的使用范围。下面这个版本就添加了**kwargs的支持。需要注意的是,kwargs不能进行hash,也就不能直接作为python中字典的key值,因此这里现将其转成一个frozenset。

这里新增加了一个product_two_number函数,用于测试func_cache中的字典cache是否对于每个被装饰的函数都分配了一个,答案是肯定的。这是因为每次处理@func_cache时,都会调用func_cache(func)一次。这种情况要与将可变变量作为函数的默认参数的情况区分开。

但是这里的Decorator还有一个问题,它改变了被装饰函数add_two_number的签名,比如:

这不是我们想要的,而且在复杂项目中,对于Bug的追踪也将是灾难性的。

好在Python为我们提供了functools模块,其中的wraps装饰器可以帮助我们解决这个问题。

到了这里,变得有些复杂了——在一个Decorator的定义里,居然出现了另一个Decorator!这该怎么理解呢。现在可以暂时不用考虑那么详细,只要把wraps装饰器当作完成某一功能的黑盒即可。之后,我们会用其他方式处理这个问题。

带参数Decorator

目前我们实现的函数缓存装饰器,会缓存所有遇到的函数返回值。我们希望能够对缓存数量上限做一个限制,从而在内存消耗和运行效率上取得折中。但是同时,对于不同的函数,我们希望做到缓存上限不同,例如对于运行一次比较耗时的函数,我们希望缓存上限大一些;反之,则小一些。这时,需要用到带参数的Decorator

先看代码实现:

我们在原来的func_cache外又包了一层outer_deco,其中含有参数size,用作函数缓存上限。但是这里的outer_deco并不是以函数对象为参数的,怎么能够作为装饰器呢?的确,严格来说,这里的装饰器仍然是func_cache,而outer_deco的作用,仅仅是利用Python闭包的特性,提供size参数。

我们注意到,outer_deco的返回值,是真正的装饰器func_cache。对比两种装饰器的使用方式:

也就是说,无参数的装饰器,@符号后面接的是一个可做Decorator的函数对象;而有参数的装饰器,@符号后面接的是一个函数调用,此函数调用返回的是一个可做Decorator的函数对象。

上面的代码中为了便于对比理解,使用了outer_deco这种无法表明装饰器功能的名字。下面将命名规范化:

至此,Python原生的Decorator就解析的差不多了。此外,Decorator还可以用于装饰类/用类实现,其实在Python中,函数和类都可以当作callable对象,所以和上面的情况大同小异。

decorator模块应用

但是,从上面的代码中也可以看出,到了带参数的Decorator这一步,Decorator的实现已经有了两层的函数嵌套,难于理解且不够优雅。此外,使用@wraps解决函数的签名保持问题,也不够完美,因为当用inspect.getfullargspec获得的函数签名依然是错误的。

这时就要引出Michele Simionato实现的decorator模块。这个模块可以不仅可以减少实现Decorator过程中的函数嵌套,还可以完美的保持函数签名不被更改。

decorator模块实现无参装饰器

首先实现最简单的无参数Decorator:

可以看到,使用了decorator模块之后,对于无参数的装饰器实现,消除了函数嵌套。同时,也是由于消除了函数嵌套,无法利用闭包特性,所以我们必须把缓存字典_cache挂在函数对象func上。

这里decorate(func, _cache)的语义也很好理解:用_cache函数来替换func函数。

使用inspect.getfullargspec也可以获得正确的函数签名:

decorator模块实现有参装饰器

接下来,我们尝试使用decorator模块实现带参数的装饰器:

实现带参数的装饰器的方式是相同的:在之前不带参数的装饰器外面再包一层函数,通过闭包将参数绑定到装饰器上,并将装饰器返回。

decorator模块中的decorator函数

最后,decorator模块还提供了一个decorator函数,它可以直接将参数列表为(func, *args, **kwargs)的函数转换成一个无参装饰器。那么对于上面的装饰器实现,可以进一步简化为:

总结

至此,Decorator的基本内容就解析完了。相关代码可以在我的GitHub页面找到。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 2 收藏 评论

关于作者:usher2007

游戏开发 C/C++ Python Linux 个人主页 · 我的文章 · 13 ·    

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部