前面的一篇博文《python 的装饰器》总结了 python 一些装饰器的语法细节。 在平常的使用中,既可以定义装饰普通函数的装饰器,也可以定义装饰类方法的装饰器。 但是由于 python 的类方法中是显式定义实例本身 self 参数的,所以在装饰器的定义中也要处理相应的 self 参数:
|
|
这就造成一个很烦人的问题:同样的功能的装饰器,但是想要装饰普通函数和类方法,却要写成两个版本的。
1. 引子
一种可能的解决思路是,找找 python 语法本身是不是有什么办法但是我不知道 。 另外一种思路是,在装饰器内能拿到传入的被装饰函数,能不能通过反射来试图分辨出它到底是普通函数还是类方法? 很遗憾的是,我用我所知道的各种方法查了各种资料,发现这两种思路好像都不行。。。
其实仔细一思考发现第二种思路是有问题的。因为 python 支持在运行期间动态的给类绑定方法, 就像把普通的整数值绑定到某个属性上一样,给它绑定方法也可以是把普通函数绑定到某个属性名称上。
所以仅仅是得到一个函数,你根本就搞不清楚它到底会被当成普通函数调用还是会被当成方法调用。
2. __call__ 和 __get__ 方法
有了上面的分析可以发现,想判断一个函数是普通函数还是方法,得要等到它真正被调用的时候才会知道。 python 提供的一些机制,可以来帮助我们判断这个问题。
2.1. __call__
在 python 这种奉行鸭子类型的语言里,你看到一个东西被当成函数调用了,并不意味着它就是函数,它也可能只是一只会像鸭子呱呱叫的一只鸡。
实际上,任何实现 __call__
方法的对象,都能被当成函数调用。这种对象称为 可调用对象 。 把这样的对象当成函数调用:
|
|
其实相当于调用其 __call__=
方法:
|
|
这有点像 C++重载()运算符。
2.2. __get__
拥有 __get__
方法的对象被称为 descriptor 。 名字是什么不重要,实际上在写正常的代码时你很少用到它,它和 python 实现一些语言特性有关系。 做为开放的 python 语言,你能清楚看到它偏底层的一些东西。
为了理解 python 的这玩意具体有什么行为,上一段代码就知道了:
|
|
<__main__.example_class object at 0xb6a196b0><class '__main__.example_class'>
Call __set__: <__main__.example_class object at 0xb6a196b0> test
可以看到,当取得一个对象属性的值的时候,如果这个属性是个 descriptor,其实是调用这个 descriptor 对象 的 __get__
方法拿到真正的值 (参数 instance 是使用这个 descriptor 的对象实例,type是这个实例的类)。
而试图给对象的一个属性赋值时,如果这个属性也是 descriptor,那么实际上会调用 descriptor 对象的 __set__
方法 (参数 instance 同上,val 是要赋给的值)。
python 提供这个特性有什么用处呢?一个通俗的说法是,可以拿来很方便的进行数据封装。 你以为这个对象的属性是一个储存了一些数据的变量,但是其实它只是一段待执行的代码而已。 python 中有一样效果的另一个功能是内置的装饰器 @property
,它也能把类中的 getter 和 setter 包装成在调用者看来是个普通属性来访问。
而我个人认为这功能在数据封装上的体现只不过提供了一种方便的语法糖罢了。即使没有这个,使用传统的 getter 和 setter 也能很好的进行数据封装。
3. 识别函数调用和方法调用
有了上面介绍的 python 的这两个特性,该回到主题了。 如果想实现对函数调用和方法调用都通用的装饰器,首先需要一个函数被调用时,你能识别它是普通函数调用还是方法调用的手段。
|
|
普通函数调用。参数: ('hello',)
通过实例<__main__.Test object at 0xb6ac8170> 调用: ('hello',)
可以看到,上面的 example 对象,当它被当成普通函数调用时,实际上会调用它的_call_方法。 而把它作为类的一部分,当作类方法调用,此时 t.example
是 __get__
方法的返回值,也就是其内部的 func 函数。 因此, t.example("hello")
也就是调用其内部创建的 func 函数。
这就意味着,example 对象,它被当成普通函数调用和当成方法调用时,会被重定向到不同的代码上。
4. 通用的装饰器
之前的基本的装饰器,是传入一个被包装的函数,它会返回一个包装后的函数。 如果返回的这个包装后的函数不是真正的函数,而是上面所说的 类似于 Example 对象的东西,把它当函数调用时,它能识别普通函数和方法调用并且重定向到不同的代码上。
那么,不就能实现我们的需求,对普通函数和类方法使用同样的一套装饰器了吗?
|
|
调用函数 add,参数: (1, 2)
调用函数 add_with,参数: (2,)
5. 更方便的使用
上面的使用方式,把之前的包装函数变成了一个充满丑陋 hack 的类,不仅降低了代码可读性,也带来了不少麻烦。 对于普通函数的装饰器的定义向这样一样简洁易读:
|
|
假如有一种方法,把 wrapper 这个包装函数,自动给转换成上面那个充满一坨无关代码的 Wrapper 对象,那就完美了。
之前的写法还有一个问题,它没有处理函数签名问题。我们尝试写一个装饰器 autoadapttomethods 封装这些复杂度:
|
|
有了这个 autoadapttomethods 装饰器,你只需要拿它装饰基本的装饰器,就能自动的让这个装饰器对普通函数和方法都有效果:
|
|
调用函数 add,参数: (1, 2)
调用函数 add_with,参数: (2,)
可以看到,本来这个 log 装饰器应该是装饰普通函数,它并没有对方法的 self 参数做处理所以对方法使用得不到期望的结果。 但是使用@autoadapttomethods 装饰器后,这个 log 就变成了对普通函数和方法都能使用的装饰器了。
最后要注意的是,这个 autoadapttomethods 装饰器是修饰基本装饰器的,如果你的装饰器是带参数的装饰器,要注意它应该修饰内层的那个基本的装饰器。
6. 结尾
本来这个功能应该是 python 本身就应该支持的语法特性,但是不知道为什么 python 并没有内置这个功能,所以才需要用这么多 hack 技巧自己实现。
从另外一个方面也能看出,python 作为一门开源的语言,其各种实现思路都是公开的,而且本身也拥有足够的灵活性,所以才能像现在这样,自己创造一些很 coooool 的特性。
本文的技术细节部分参考自 http://stackoverflow.com/questions/1288498/using-the-same-decorator-with-arguments-with-functions-and-methods, 特此感谢这些人的无私奉献。