Abstract
本文将从三个问题来阐述Python Descriptor:
- 什么是Descriptor?
- Descriptor如何被调用?
- Non Data Descriptor 与 Data Descriptor有何区别?
在回答完这三个问题之后,本文还将列举几个使用Descriptor的例子:
@Property原理
基于Property实现@cached_property
Python函数和方法的区别
如果之前不了解Python Descriptor,建议先过一遍Descriptor How To Guide。对于英语不过关的同学,也可以看一下中文资源。
什么是Descriptor?
1 | class Descriptor(object): |
如果一个继承于object的类实现了__get__,__set_\,__delete__中其中任意一种方法,那么该类的对象就是一个descriptor,在上述例子中a.x就是一个描述器。
为什么一定要继承于object呢?因为descriptor机制只作用于新式类。
Descriptor如何被调用?
Descriptor的调用机制是基于Python的属性访问机制的,因此弄清楚Python的属性访问机制是很有必要的。
Before Descriptor
在出现Descriptor机制之前,如果访问a.x属性,则Python属性默认的查找顺序是:
After Descriptor
在出现Descriptor之后,Python对象属性的访问机制就出现了变化,下面以Python代码模拟这一个寻找过程:
1 | def object_getattr(obj, name): |
在上面的Python代码中很容易可以看到How to Guide中强调的Descriptor优先级是如何保证的:Data Descriptor > Object Attribute > Non Data Descriptor。
有一个想跟大家分享比较特殊的例子是,如果在obj.__dict__中找到的对象是一个descriptor,那么descriptor机制并不会被调用,即a.__dict__[‘x’] = Descriptor(),那么a.x并不会调用__get__方法。
1 | class Descriptor(object): |
相比Python对象属性的访问机制,Python类属性的访问机制有一点区别:
- 在Python中,类其实也是一种对象,只不过类是通过MetaClass(元类)生成的,因此Python类属性的访问会将上述的class_lookup函数替换为metaclass_lookup函数。
- Object Attribute的查找替换为对class.__mro__的遍历查找,并且在这个查找过程中会判断找到的对象是否拥有__get__方法,如果拥有则调用。
- 类属性访问机制调用的_get\_方法传入参数时,obj参数传入None,type参数传入class。
希望更详细地探究Python类属性访问机制的同学可以看这里:object-attribute-lookup-in-python
Non Data Descriptor 与 Data Descriptor有何区别?
Non Data Descriptor只需实现__get_方法,Data Descriptor需要同时实现\_get__方法和__set__方法。
如果要实现一个只读的Data Descriptor,那么只需要在__set__方法中抛出异常即可。
Data Descriptor和Non Data Descriptor最大的区别就是上述的优先级问题,当descriptor与obj.__dict__中一个属性同名时:
- 如果descriptor是一个Data Descriptor,那么返回descriptor.__get__的调用值。
- 如果descriptor是一个Non Data Descriptor,那么返回obj.__dict__中的属性值。
1 | class NonDataDescriptor(object): |
@Property原理
Property在Python源码中被实现为一个类,通过Property可以快速定义一个Data Descriptor。
Property的纯Python代码实现如下:
1 | class Property(object): |
Property的使用方法一般有两种:
1 | # 第一种使用方法: |
有时候常常被@这个符号所迷惑,其实@符号基本可以等价为a = property(a)。
在使用第一种方法时,需要注意setter装饰的方法必须跟property装饰的方法同名,否则setter装饰器将不起作用,stackoverflow上也有关于这个问题的讨论。
基于Property实现@cached_property
@cached_property装饰器是一种很常用的轮子,在Django和Werkzeug中都有类似的实现。在实现的时候需要注意以下两点:
- 实例的更改是否会污染类变量
- 更新实际的value时是否同时更新了缓存中的值
这是是我一开始写的一个错误实现:
1 | class CachedProperty(property): |
这一种实现哪里出错了呢?它的实例污染了全局变量:
1 | In [11]: c1 = Container() |
因此缓存的value不应该绑定在CachedProperty的实例上面,否则每一个Container的实例都能改变其他实例的attr属性,造成了类变量的污染。
正确的实现应该是每个实例的缓存绑定在各自的__dict__变量中:
1 | class CachedProperty(property): |
Python函数和方法的区别
Function在Python中被实现为一个Non Data Descriptor,以下是Python代码表示的Function:
1 | class Function(object): |
当在类中定义Function的时候,如果直接访问类的__dict__变量,仍能得到一个Function object,此时拿到的还不是一个方法,因为Function 还没有跟实例绑定。
1 | class A(object): |
- 当从实例调用方法时,a.test等价于types.MethodType(test, a, None),此时返回一个bound method,即test function已经绑定了实例a。
- 当从类调用方法时,A.test等价于types.MethodType(test, None, A),此时返回的是unbound method。
@classmethod
classmethod需要绑定class,以下是Python代码实现:
1 | class classmethod(object): |
因此在Python中如果一个类的方法使用了@classmethod,即使从实例调用这个方法,传进去的第一个参数仍然是class。
@staticmethod
staticmethod不需要绑定class,以下是Python代码实现:
1 | class staticmethod(object): |
参考资料: