0%

一文彻底搞懂 Python中的描述器、反射

描述器

什么是描述器?

一个类中定义了如下一个或多个魔术方法,这个类的实例就是描述器:

__get__,__set__,__delete__

通常需要两个类来构建描述器:

如果类B的类属性x,指向另一个类A的实例。被指向的A的实例就是描述器对象。B.x是描述器,B也是描述器的属主(owner)。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
def __get__(self, instance, owner):
pass

def __set__(self, instance, value):
pass

def __delete__(self, instance):
pass

class B:
x = A() # x是描述器
pass

类属性的值,通常是一些已有类型的对象,比如字符串、列表等。

当使用了描述器,类属性就指向一个描述器对象,描述器通过三个魔术方法,可以自定义属性的行为。

描述器的分类

非数据描述器:

只定义了__get__,就是非数据描述器(non-data descriptor)。

数据描述器:

定义了__get__,且定义了__set____set__ 与 __delete__,就是数据描述器(data descriptor)。

属性搜索顺序

当一个实例与它所属的类有相同的属性名时:

非数据描述器,实例的属性搜索顺序:

__getattribute__ ⟶ 默认搜索顺序 [1] ⟶ __getattr__。也就是说,此时__get__无效。

数据描述器,会拦截实例属性字典的访问:

不会访问实例属性字典__dict__。属性访问或修改会被描述器的__get__, __set__, __delete__方法处理。

注意:

如果有 __getattribute__ 方法,不管有没有描述器,实例属性搜索时,都优先调用此方法,可以拦截一切(包括 实例.__dict__ 的访问也拦截)。

__getattribute__ 和 __getattr__ 又是做什么的?下文讲。

[1] 默认搜索顺序:
默认搜索顺序就是没有描述器时的搜索顺序,遵循如下规则:
实例的属性字典(__dict__) ⟶ 类的属性字典 ⟶ 类的父类的属性字典 ⟶ … ⟶ 祖先类object的属性字典

属性搜索顺序与类的继承有关。如果是单继承,属性(或方法)搜索路径是确定的,一直向上找。如果是多继承,就涉及到MRO(方法解析顺序)。Python3的MRO采用C3算法,在类被创建出来的时候,就计算出一个MRO有序列表。关于C3算法,见官方文档

属性读写操作示例

B.x = 400,类属性赋值(赋值即重新定义),如果x是描述器,将被覆盖。
b.x = 500,非数据描述器时,将修改实例自己的属性(__dict__)。
b.x = 600,数据描述器时,将调用描述器的__set__方法。

B.x,若x是描述器,调用描述器的__get__方法。
b.x,若x是描述器,调用描述器的__get__方法。

直接操作实例的__dict__字典,可以绕开描述器对__get____set__等的调用。

举例:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class A:
def __init__(self):
print('A().init')
self.x = 101 # 这是A自己的实例,与B无关

def __get__(self, instance, owner):
print('~~~~ A.get ~~~~')
print(self) # A的实例本身
print(instance) # B.x访问时,为None。b.x访问时,为B的实例对象
print(owner) # 类B
print('~~~~ A.get ~~~~')
return getattr(instance, 'z', 'no_z_found') # 查找b.z时,又会回去调用B.__getattribute__

def __set__(self, instance, value):
print('~~~~~ A.set ~~~~')
print(self) # A的实例本身
print(instance) # 修改b.x时,才进入此方法,instance为B的实例对象
print(value) # 赋给 b.x 的值
print('~~~~~ A.set ~~~~')
instance.z = value # 演示,把b.x的值保存到b.z,而不是b的属性字典__dict__

def __delete__(self, instance):
print('~~~~~ in delete')
del instance.z

class B:
# 创建描述器x
x = A()

# 定义如下方法,实例属性访问最先调用它,但在类A中定义无用
def __getattribute__(self, item):
print('___ in getattribute ___')

# 查找b.x时,此处又会调用A.__get__(因为x是描述器),而不是调B.__getattribute__,不然会递归
return object.__getattribute__(self, item)

def __init__(self):
print('B().init')
self.x = 1000 # 非数据描述器时,实例修改自己的属性,self.x会访问实例自己的__dict__
# 数据描述器时,调用 A.__set__

b = B() # 先生成A的实例,即执行A.__init__,生成描述器对象,然后执行B.__init__

# 属性访问
print('\n' + '-' * 30)
print(B.x)
print()
print(b.x)

# 属性修改
# 覆盖描述器
#B.x = 123
#print(B.x)
print('\n' + '#' * 30)

# 非数据描述器时,b修改自己的属性,赋值即重新定义,覆盖描述器x
# 数据描述器时,b.x 调用 A.__set__
b.x = 456
print(b.x)

print('\n' + '=' * 30)
print(b.__dict__) # 甚至访问b的属性字典,也是调用B.__getattribute__
del b.x # 删除属性,会调用 A.__delete__
print(b.x)
print(b.__dict__)

反射

上文提到的__getattribute__跟__get__有什么关系呢?实际上前者是反射相关的魔术方法。那什么是反射呢?

当我们需要用到对象的某个属性(或方法),但是由于某种原因无法确定这个属性是否存在,这时我们需要用一种特殊的机制,去访问和操作这个未知的属性,这种机制就称为反射(reflection)。反射就相当于一种自我检查机制。

反射机制不仅包括,要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息,改变程序状态或结构。总之一句话,反射指的是运行时获取类型定义信息,并且还能修改这些信息。

与反射相关的四个函数:getattr、setattr、delattr、hasattr。与这些函数相关的四个魔术方法:__getattr__, __setattr__, __delattr__, __getattribute__。见下面表格:

魔术方法 含义
__getattr__ 此方法只影响实例。实例属性默认搜索顺序:实例自己(的__dict__,后同)、实例的类、类的父类、父类的父类、object祖先类。若从这个顺序中没有找到属性,会抛出AttributeError异常,但类中定义了__getattr__,实例将捕获异常,并调用此方法。此方法可用于实例没有找到属性时,拦截异常,做一些操作。
__setattr__ 此方法只影响实例。self.x = x, setattr(self, ‘x’, x) 等涉及到修改实例属性的操作时,如果定义了__setattr__,就会调用此方法。此方法可以拦截实例属性修改操作的默认行为。比如将实例的属性存储在新的字典中,而不是存储在默认字典__dict__。
__delattr__ 此方法只影响实例。del self.x, delattr(self, ‘x’) 等涉及删除实例属性的操作时,如果定义了__delattr__,将会调用此方法。
__getattribute__ 此方法只影响实例。实例的所有属性的访问,第一个就调用此方法。此方法能完全控制属性的默认访问顺序。可以在此方法中做一些处理,然后手动抛出AttributeError异常,这将继续调用__getattr__方法(如果有的话)。

__getattr__ VS __getattribute__

两者的执行时间点不同。

前者会在默认属性搜索顺序中未找到属性时,拦截异常,并执行。

后者会在第一时间执行,完全拦截默认属性搜索顺序。两者执行顺序如下:

__getattribute__实例属性的默认搜索顺序__getattr__

举例:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class A:
def __init__(self, x, y):

# 用kv赋值方式增加属性 不会调用__setattr__
self.__dict__['a'] = 7
self.__dict__['_d'] = {}

# 如下属性将会存储到新字典_d 会调用__setattr__
self.x = x
self.y = y

def __getattr__(self, item):
print('_in getattr:', item)

# 属性未找到时 才调用__getattr__ 并从新字典返回属性
return self._d[item]

def __setattr__(self, key, value):
print('_in setattr:', key, value)

# 下面写法都会递归 它们都调用__setattr__
# self.key = value
# setattr(self, str(key), value)

# 用新字典存储属性
# _d 属性在实例的__dict__,因为是字典操作,所以等号左边的self._d就不会调用__getattr__
self._d[key] = value

def __delattr__(self, item):
print('_in delattr:', item)
del self._d[item]

a = A(4, 5)
print(a.__dict__) # 打印属性字典,不会调用A.__getattr__,除非定义了_getattribute__
print(a.x)
del a.x
delattr(a, 'y')
a.t = 123 # 这也会调__setattr__
print(a.__dict__)
print('=' * 30)

class A:
d = {}
def __init__(self, x, y):
self.x = x
self.y = y

def __getattribute__(self, item):
print(item, '~~~~~~~')

# 推荐写法
return object.__getattribute__(self, item)

a = A(3, 4)
print('#' * 30)
print(a.x, a.d)

如果上面提及的魔术方法同时存在,会怎么样呢?详见 Python中的属性搜索顺序

描述器的应用

用描述器实现ClassMethod、StaticMethod(非数据描述器的应用):

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 非数据描述器 实现StaticMethod
class StaticMethod(object):
def __init__(self, fn):
self.fn = fn

def __get__(self, instance, owner):
print('_in StaticMethod')
print(self, instance, owner)
return self.fn


# 非数据描述器 实现ClassMethod
class ClassMethod(object):
def __init__(self, fn):
self.fn = fn

def __get__(self, instance, owner):
print('_in ClassMethod')
print(self, instance, owner)
return partial(self.fn, owner) # 固定fn的owner参数,就是固定bar函数的所属类A2


class A2(object):
AGE = 20

def __init__(self, name, age):
self.name = name
self.__age = age

# 装饰器语法
@StaticMethod # foo=StaticMethod(foo) 构建非数据描述器对象foo
def foo(x):
print('_in foo')
return x

# 装饰器语法
@ClassMethod # bar=ClassMethod(bar) 构建非数据描述器对象bar
def bar(cls, x):
print('_in bar')
return cls.AGE, x

a = A2('Tom', 19)

# 这里分两步,a.foo读取属性foo,会调用__get__返回A2里定义的foo函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(3),以此来实现静态方法foo
print(a.foo(3))
print('=' * 30)

# 这里分两步,a.bar读取属性bar,会调用__get__返回A2里定义的bar函数,这个函数其实是描述器的属性fn,
# 然后调函数fn(4),cls参数已固定,实现了类方法
print(a.bar(4))

用描述器实现Property(数据描述器的应用):

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Property(object):
def __init__(self, fget, fset=None, fdel=None):
self.fget = fget # fget是A的方法age
self.fset = fset
self.fdel = fdel

def __get__(self, instance, owner):
# fget是self实例的属性,不是Property的方法,所以不会把self自动传递给fget,所以需要instance参数
return self.fget(instance)

def __set__(self, instance, value):
self.fset(instance, value)

def __delete__(self, instance):
self.fdel(instance)

def setter(self, fset):
self.fset = fset
return self # 必须返回Property实例才能构建描述器

def deleter(self, fdel):
self.fdel = fdel
return self


class A(object):
def __init__(self):
self.__age = 13

# age=Property(age)=描述器对象
# @Property必需在@age.setter与@age.deleter的前面
# 因为@Property创建了描述器对象add,下面才能使用add对象
@Property
def age(self):
return self.__age

# age=age.setter(age)=描述器对象
# 因为age是描述器对象,指向Property的实例,该实例有属性setter
@age.setter
def age(self, value):
self.__age = value

@age.deleter
def age(self):
del self.__age


a = A()
print(a.age) # 把方法调用变成了属性访问
a.age = 17 # 调__set__
print(a.age)
del a.age # 调__delete__