Python 中对象属性的访问过程
一个python对象的属性有以下几种:
- 普通属性
- 数据描述符(data descriptor)
- 非数据描述符(non-data descriptor)
- 方法描述符(method descriptor)
- 成员描述符(member descriptor)
以上的几种描述符都可以通过 inspect 模块进行进行检查, 本文主要涉及前面3种属性的查找过程.
__getattr__ 特殊方法
python中的’.’属性访问运算符, 通过 __getattribute__ , 如果 __getattribute__ 方法抛出 AttributeError 异常, 将会继续调用 ___getattr\_ (如果有定义的话, 否则抛出AttributeError异常), __getattr__ 的一个用法做属性访问的委托, eg:
1 | class Heart: |
解释一下 me.color 这句
- Person.__getattribute__(me, “color”) 结果是引发一个 AttributeError, 该异常导致 __getattr__ 的调用
- Person.__getattr__(me, “color”), 按照定义的行为,将会把 me.heart.color 作为结果返回
__getattribute__ 特殊方法
__getattribute__ 方法是Python新式类才有的, 主要用于自定义属性访问控制. eg
1 | class Foo: |
上面的例子展示一个demo, 主要用来向标准输出, 打印出访问的属性的名称.
foo.finalanswer 调用了 foo.__getattribute__(“finalanswer”), 该方法先把 finalanswer 打印出来,然后调用父类默认的__getattribute__ 函数, 得到这个属性的值. 那么 __getattribute__ 函数的具体行为是怎么样的呢? 也就是python对一个对象的属性查找是怎么样的呢?
对普通属性的查找
先上一个例子先:
1 | class A: |
上面的例子中通过 a.mirror = b 为对象 a 赋值了一个属性 b, 最终 b 通过 “mirror”: b 这种key-value 的形式存储在 a.__dict__ 中, 而对于mirror的查找将会转化成为 a.__dict__[“mirror”], 也就是说, 在python中对普通属性的访问, 有如下关系:
1 | obj.attributename ==> obj.__dict__["attributename"] |
但是呢, 不知道你注意到没有, obj.__dict__ 也是一种属性查找, 那么对它的访问是否也遵循上面的规则呢? 如果是的话, 应该有如下的转化:
1 | obj.attributename ==> obj.__dict__["attributename"] ==> obj.__dict__["__dict__"]["attributename"] ==> ... |
一个无限的递归…所以, 显然对 obj._dict\_的访问并不按照上面描述的规则进行转换, 那么这种访问的转化规则是怎么样的呢?
对数据描述符属性的查找
上面说到, 如果按照普通属性的查找方式来查找 __dict__ 的话,将会产生无限递归, 为了解决这个问题,python定义了数据描述符的概念(datadescriptor), 即如果一个类同时定义了__get__, __set__, __delete__ 方法, 那么它就是数据描述符.
1 | class Descriptor: |
上面展示了一个datadescriptor的访问控制, 其中name = Descriptor() 定义了一个数据描述符, 总结起来对一个数据描述符属性的访问有如下的规则:
对于实例对象来说, 对数据描述符的访问将会转化对数据描述符的__get__ 的调用:
a.name 将会转化成为 A.name.__get__(a, A) 的调用对于类对象来说, 对数据描述符的访问将会转化成为对其 __get__的调用
A.name 将会转化成为 A.name.__get__(None, A)
对于实例对象来说, 对 数据描述符属性进行赋值将会转化成为对其 __set__ 的调用:
a.name = 23 ===> A.name.__set__(a, 23)
如果对数据描述符进行删除, 将会 调用其 __delete__ :
del a.name ===> A.name.__delete__(a)
回到我们刚才的话题, 对__dict__ 的访问是按照什么方式进行的呢? 首先看看 __dict__ 是什么
1 | class A: pass |
从上面可以看处, A.__dict__[“__dict__“] 是一个数据描述符, 那么, a.__dict__ 是不是就是从这个数据描述符中来的呢?
1 | a.__dict__ is A.__dict__["__dict__"].__get__(a, A) |
所以,是的. 完整的 foo.finalanswer 的查找过程如下:
- foo.finalanswer 转化成为 foo.__dict__[“finalanswer”]
- foo.__dict__ 转化成为 Foo.__dict__[“__dict__“].__get__(foo, Foo)
- 最终转化成为Foo.__dict__[“__dict__“].__get__(foo, Foo)[“finalanswer”]
非数据描述符属性的查找
有数据描述符, 当然也有非数据描述符了, 它的定义比数据描述符简单:
- 定义了 __get__ 方法的类的实例,都是非数据描述符
那么, 它跟数据描述符的区别是什么呢? 看看下面的例子
1 | class NonDataDescriptor: |
从上面的例子可以看处来, 对于非数据描述符的属性查找来说, 其查找过程还是跟数据描述符的属性查找过程一样的, 但是对其惊醒赋值,将会以相应的key:value 形式将赋值的结果存储在 a.__dict__ 中, 后续对该属性名称的查找就不会调用非数据描述符的__get__ 方法了.
类本身也是对象
类本身也是对象, 在上面的类实例的查找中,有如下转化关系:
1 | foo.__dict__ ===> Foo.__dict__["__dict__"].__get__(foo, Foo) |
其中, type(obj).__dict__ 也有如下的转换
1 | type(obj).__dict__ ===> type(type(obj)).__dict__["__dict__"].__get__(type(obj), type(type(obj))) |
转化之后又有 .__dict__ 的查找, 似乎又会有无限递归的情况出现, but
1 | type(Foo) |
也就是说 type(Foo) 和 type(type(Foo)) 的运算结果是一样的, 所以,不会递归了.
1 | class A: |
参考阅读
[1] https://docs.python.org/3/howto/descriptor.html
[2] https://docs.python.org/3/library/inspect.html#module-inspect
[3] http://www.cs.utexas.edu/~cannata/cs345/Class%20Notes/15%20python_attributes_and_methods.pdf
[4] http://www.cs.utexas.edu/~cannata/cs345/Class%20Notes/15%20Python%20Types%20and%20Objects.pdf