在上一篇文章中,我们简单回顾了Python中在继承语境下的属性解析顺序,同时补充了能够控制、影响属性解析的3个函数/方法(2个魔术方法 + 1个内置函数),相信对Python中属性的解析,相较于MRO,有了更进一步的认识。
今天这篇文章中,我们将考虑属性描述符存在的情况下,对于Python中的属性解析顺序又会产生怎样的影响,从而给出一个更加完整的实例对象的属性解析顺序。
属性描述符的种类前面已经介绍过属性描述符的定义及使用,其实属性描述符根据所实现的魔术方法的不同,可以分为两种类型:
1、数据描述符:同时定义了__get__()和__set__()方法,或者定义了__set__()方法(仅定义__set__,其实没有太大意义)的属性描述符为“数据描述符”。
2、非数据描述符:仅定义了__get__()方法的描述符,称为“非数据描述符”。
接下来,我们分别定义一个数据描述符和非数据描述符,直接看代码:
执行结果:
代码中,我们分别定义了一个数据描述符和一个非数据描述符。其中,数据描述符是一个整数的属性描述符,用于控制属性的合法取值范围。非数据描述符定义了一个打工人所属组织的一个初始默认值,实现的功能是如果一个实例对象没有重新设置team,则始终返回默认值,一旦设置了team属性,则取属性自身的team取值,而不会影响到新的实例对象的取值。
通过代码及执行结果,我们可以大概得出以下结论:
1、数据描述符其实是将属性整个托管给描述符机制了,不管是对属性的访问还是修改,都是基于描述符实现的,相关的数据不会在实例对象的命名空间也就是__dict__字典中体现。
2、即使我们手动在实例对象的__dict__中显式添加一个与数据描述符同名的属性,通过“点”操作符访问到的仍然是数据描述符对应的属性。
3、不同于数据描述符的统一接管,非数据描述符只接管了属性的访问操作。而且,一旦对该属性进行了修改操作,则会在实例对象的命名空间__dict__字典中添加同名属性,后续对该属性的访问,都是对__dict__中的同名属性的访问及修改了。
完整的属性解析顺序首先给出相对完整的属性解析顺序的结论,之后再通过代码进行演示验证结论。
当通过实例对象“点”操作符访问属性或者等价的getattr()内置函数的形式访问属性时,会按照以下顺序进行属性的解析:
1、首先调用__getattribute__()魔术方法,进行统一的属性访问控制逻辑的执行。
2、如果要访问的属性时数据描述符,则__getattriubte__()方法的内部会进行数据描述符__get__()方法的调用,返回相应的属性值,属性解析结束。
3、如果属性在实例对象的命名空间__dict__字典中,则直接返回,属性解析结束。
4、如果属性在实例对象所属类的命名空间,即__class__.__dict__字典中,则直接返回,属性解析结束。
5、如果属性在示例对象所属类的基类(按照MRO顺序进行解析查找)的__dict__字典中,则直接返回,属性解析结束。
6、如果存在同名的非数据描述符,则调用其__get__()方法,返回属性值,属性解析结束。
7、如果实例对象所属类有定义__getattr__()方法,则调用__getattr__()方法,属性解析结束。
8、属性解析失败,抛出AttributeError。
对应的流程图如下:
接下来,以一个完整的代码示例,来演示属性解析顺序:
执行结果:
总结本文介绍了属性描述符的种类,并比较了不同的属性描述符在属性解析时的差异,最后结合属性描述符、__getattribute__()、__getattr__()及MRO等,给出了一个相对完整的属性解析顺序。
需要说明的是,属性描述符及后面的文章中要介绍的元类的概念,在通常意义的业务场景中是很少用到的。但是,如果涉及到框架的开发或者需要阅读框架的源码时,对这些内容的掌握还是很有必要的。
感谢您的拨冗阅读。如果对您学习Python有所帮助,欢迎点赞、收藏。