一: python描述符官方定义
In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor。
概括来讲就是一个对象定义了__get__()、__set__()、__delete__()中的任一方法即为描述符。
二:我们先来了解下每个对象都具备的属性__dict__
字典结构, 存放着对象的所有属性
对象属性的查找顺序为:
- 实例属性
- 类属性
- 父类属性
- __getattr__
以上查找顺序请自己验证。
上图可以得出cls_x属性是属于A类的,而不是实例的属性。
三: 下边来看下魔法函数, __get__, __set__, __delete__
方法的原型为:
def __get__(self, obj, objtype)
def __set__(self, obj, val)
- def __delete__(self, obj)
self, obj, objtype 到底指的什么,请看下面程序输出:
程序输出:
从输出中可以看出Mytest实例test在访问属性desc时, 会自动调用Desc的__get__方法:
- self 即为Desc实例, 也就是MyTest属性desc.
- obj 即为test, MyTest实例
- objtype 输出中可以看出是MyTest, 所有的一切都是由其开始的。
到此, 我们可以看到Desc其实就是一个描述符, 换而言之描述符就是一个类, 因为其定义了__get__, __set__, __delete中的任一方法。
四: 揭示描述符的诸多疑点
- 为什么访问test的desc属性, 会直接去访问描述符的__get__方法呢?
从我们博文开始讲的实例查询属性的顺序先访问类的__getattribute__, 如果没有找到, 进一步查找类属性, desc到此找到了!!!
判断属性 desc为一个描述符,此时,它就会做一些变动了,将 MyTest.x 转化为 desc.__dict__['x'].__get__(obj, MyTest) 来访问了。
2. 上述实验中desc是MyTest的一个类属性, 那描述符能不能作为实例属性呢?
请看输出结果:
???为什么test实例对象没有desc2呢!, 在访问属性desc2时发现是描述符会将访问__getattribute__转换为MyTest.__dict__('desc2').__get__(test, MyTest), MyTest类是没有desc2类属性的所以发生了异常。
3. 如果实例属性与描述符属性相同, python解释器该如何处理呢?
输出结果为:
理论上讲, test实例对象应该是有desc属性的, 不应该还去调用描述符的__get__方法, 从第二行的实例的__dict__中不难看出并没有desc属性了, 这就说明实例属性与描述符属性同名时会被覆盖掉。
我们再进一步删掉描述符中__set__方法呢, 且看输出结果
实例的desc属性回来了!!!, 所以也就没有再去访问描述符的__get__方法。
这还是属性查找顺序的问题, 一个类,如果只定义了 __get__() 方法,而没有定义 __set__(), __delete__() 方法,则认为是非数据描述符; 反之,则成为数据描述符, 数据描述符的优先级是高于实例属性的。
属性查找顺序的总结如下:
1.__getattribute__(), 无条件调用
2. 数据描述符:由 ① 触发调用 (若人为的重载了该 __getattribute__() 方法,可能会调职无法调用描述符)
3. 实例对象的字典(若与描述符对象同名,会被覆盖哦)
4. 类的字典
5. 非数据描述符
6 父类的字典
7. __getattr__() 方法
五:实际应用举例
试想如果我们创建学生对象, 学生对象的分数属性是0<=score<100, 如何检测属性是否正常?
请看代码示例:
输出结果是什么呢? 请看:
可见实现了我们分数属性赋值范围的检测功能。
此时我们进一步讨论下property到底是个什么东东?能否使用描述符去实现一个类似的property呢?
这样也达到了我们想要的效果, 大家可以发散思维, 多多思考描述符数据结构的用法。