没脚的雀

Python中对象属性的访问

Python 中对象属性的访问过程

一个python对象的属性有以下几种:

  1. 普通属性
  2. 数据描述符(data descriptor)
  3. 非数据描述符(non-data descriptor)
  4. 方法描述符(method descriptor)
  5. 成员描述符(member descriptor)

以上的几种描述符都可以通过 inspect 模块进行进行检查, 本文主要涉及前面3种属性的查找过程.

__getattr__ 特殊方法

python中的’.’属性访问运算符, 通过 __getattribute__ , 如果 __getattribute__ 方法抛出 AttributeError 异常, 将会继续调用 ___getattr\_ (如果有定义的话, 否则抛出AttributeError异常), __getattr__ 的一个用法做属性访问的委托, eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Heart:
def __init__(self, color):
self.heartbeat = 42
self.color = color

class Person:
def __init__(self, heart):
self.heart = heart

def __getattr__(self, attrname):
try:
return getattr(self.heart, attrname)
except AttributeError as e:
print(e)
return None

>>> myheart = Heart(color="black")
>>> me = Person(heart=myheart)
>>> me.color
'black'

解释一下 me.color 这句

  • Person.__getattribute__(me, “color”) 结果是引发一个 AttributeError, 该异常导致 __getattr__ 的调用
  • Person.__getattr__(me, “color”), 按照定义的行为,将会把 me.heart.color 作为结果返回

__getattribute__ 特殊方法

__getattribute__ 方法是Python新式类才有的, 主要用于自定义属性访问控制. eg

1
2
3
4
5
6
7
8
9
10
class Foo:
def __getattribute__(self, attrname):
print("access for attribute {attrname}.".format(attrname=attrname))
return super().__getattribute__(attrname)

>>> foo = Foo()
>>> foo.finalanswer = 42
>>> foo.finalanswer
access for attribute filnalanswer.
42

上面的例子展示一个demo, 主要用来向标准输出, 打印出访问的属性的名称.

foo.finalanswer 调用了 foo.__getattribute__(“finalanswer”), 该方法先把 finalanswer 打印出来,然后调用父类默认的__getattribute__ 函数, 得到这个属性的值. 那么 __getattribute__ 函数的具体行为是怎么样的呢? 也就是python对一个对象的属性查找是怎么样的呢?

对普通属性的查找

先上一个例子先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A:
pass

>>> a = A()
>>> b = A()
>>> b
<__main__.A object at 0x7f885ad38e48>
>>> a.mirror = b
>>> a.__dict__
{'mirror': <__main__.A object at 0x7f885ad38e48>}
>>> a.mirror
<__main__.A object at 0x7f885ad38e48>
>>> b is a.__dict__["mirror"]
True

上面的例子中通过 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
2
obj.attributename ==> obj.__dict__["attributename"] ==> obj.__dict__["__dict__"]["attributename"] ==> ... 
obj.__dict__["__dict__"]...["__dict__"]["__dict__"]["attributename"]

一个无限的递归…所以, 显然对 obj._dict\_的访问并不按照上面描述的规则进行转换, 那么这种访问的转化规则是怎么样的呢?

对数据描述符属性的查找

上面说到, 如果按照普通属性的查找方式来查找 __dict__ 的话,将会产生无限递归, 为了解决这个问题,python定义了数据描述符的概念(datadescriptor), 即如果一个类同时定义了__get__, __set__, __delete__ 方法, 那么它就是数据描述符.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Descriptor:
def __get__(self, ins, cls):
print("{ins} of {cls} ask for value.".format(ins=ins, cls=cls))
return 42
def __set__(self, ins, value):
print("{ins} want to set attr to {value}.".format(ins=ins, value=value))

def __delete__(self, ins):
raise AttributeError("Couldn't delete the attribute.")

class A:
name = Descriptor()

>>> a = A()
>>> a.name
<__main__.A object at 0x7f6778572a90> of <class '__main__.A'> ask for value.
42
>>> a.name = 23
<__main__.A object at 0x7f6778572a90> want to set attr to 23.
>>> del a.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in __delete__
AttributeError: Couldn't delete the attribute.
>>> import inspect
>>> inspect.isdatadescriptor(A.__dict__["name"])
True

上面展示了一个datadescriptor的访问控制, 其中name = Descriptor() 定义了一个数据描述符, 总结起来对一个数据描述符属性的访问有如下的规则:

  1. 对于实例对象来说, 对数据描述符的访问将会转化对数据描述符的__get__ 的调用:
    a.name 将会转化成为 A.name.__get__(a, A) 的调用

  2. 对于类对象来说, 对数据描述符的访问将会转化成为对其 __get__的调用

    A.name 将会转化成为 A.name.__get__(None, A)

  3. 对于实例对象来说, 对 数据描述符属性进行赋值将会转化成为对其 __set__ 的调用:

    a.name = 23 ===> A.name.__set__(a, 23)

  4. 如果对数据描述符进行删除, 将会 调用其 __delete__ :

    del a.name ===> A.name.__delete__(a)

回到我们刚才的话题, 对__dict__ 的访问是按照什么方式进行的呢? 首先看看 __dict__ 是什么

1
2
3
4
5
6
7
8
9
class A: pass
a = A()
>>> a.__dict__
{}
>>> A.__dict__["__dict__"]
<attribute '__dict__' of 'A' objects>
>>> import inspect
>>> inspect.isdatadescriptor(A.__dict__["__dict__"])
True

从上面可以看处, A.__dict__[“__dict__“] 是一个数据描述符, 那么, a.__dict__ 是不是就是从这个数据描述符中来的呢?

1
2
>>> a.__dict__ is A.__dict__["__dict__"].__get__(a, A)
True

所以,是的. 完整的 foo.finalanswer 的查找过程如下:

  1. foo.finalanswer 转化成为 foo.__dict__[“finalanswer”]
  2. foo.__dict__ 转化成为 Foo.__dict__[“__dict__“].__get__(foo, Foo)
  3. 最终转化成为Foo.__dict__[“__dict__“].__get__(foo, Foo)[“finalanswer”]

非数据描述符属性的查找

有数据描述符, 当然也有非数据描述符了, 它的定义比数据描述符简单:

  1. 定义了 __get__ 方法的类的实例,都是非数据描述符

那么, 它跟数据描述符的区别是什么呢? 看看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NonDataDescriptor:
def __get__(self, ins, cls):
print("{ins} of {cls} get attribute.".format(ins=ins, cls=cls))
return 42

class A:
name = NonDataDescriptor()

a = A()
>>> a.name
<__main__.A object at 0x7f6778572b00> of <class '__main__.A'> get attribute.
42
>>> a.name = "the most handsome boy."
>>> a.name
"the most handsome boy."
>>> a.__dict__
{"name": "the most handsome boy."}

从上面的例子可以看处来, 对于非数据描述符的属性查找来说, 其查找过程还是跟数据描述符的属性查找过程一样的, 但是对其惊醒赋值,将会以相应的key:value 形式将赋值的结果存储在 a.__dict__ 中, 后续对该属性名称的查找就不会调用非数据描述符的__get__ 方法了.

类本身也是对象

类本身也是对象, 在上面的类实例的查找中,有如下转化关系:

1
2
3
foo.__dict__ ===> Foo.__dict__["__dict__"].__get__(foo, Foo)
更加通用的一个形式是:
obj.__dict__ ===> type(obj).__dict__["__dict__"].__get__(obj, type(obj))

其中, type(obj).__dict__ 也有如下的转换

1
type(obj).__dict__    ===>   type(type(obj)).__dict__["__dict__"].__get__(type(obj), type(type(obj)))

转化之后又有 .__dict__ 的查找, 似乎又会有无限递归的情况出现, but

1
2
3
4
5
>>> type(Foo)
<class 'type'>
>>> type(type(Foo))
<class 'type'>
# in python3, type is class, class is type

也就是说 type(Foo) 和 type(type(Foo)) 的运算结果是一样的, 所以,不会递归了.

1
2
3
4
5
6
7
8
9
class A:
pass
a = A()
assert a.__class__ == A
assert id(a.__dict__) == A.__dict__["__dict__"].__get__(a, A)
assert A.__class__ == type
assert id(A.__dict__) == type.__dict__["__dict__"].__get__(A, type)
assert type.__class__ == type
assert id(type.__dict__) == type.__dict__["__dict__"].__get__(type, type)

参考阅读

[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

大佬给口饭吃咧