# 描述器协议概述
# 定义和简介
一个描述器是一个包含 “绑定行为” 的对象,对其属性的访问被描述器协议中定义的方法覆盖。这些方法有:__get__(),__set__()和 __delete__()。如果某个对象中定义了这些方法中的任意一个,那么这个对象就可以被称为一个描述器。
属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。例如,a.x 的查找顺序会从 a.__dict__['x'] 开始,然后是 type(a).__dict__['x'],接下来依次查找type(a) 的基类,不包括元类。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。
描述器是一个强大而通用的协议。 它们是特征属性、方法静态方法、类方法和 super() 背后的实现机制。 在 Python 内部被广泛使用来实现自 2.2 版中引入的新式类。 描述器简化了底层的 C 代码并为 Python 的日常程序提供了一组灵活的新工具。
# 描述器分类
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
数据描述符、强制描述符:同时实现__get__() 和 __set__()
非数据描述符、遮盖型描述符:仅定义 __get__()
覆盖型描述符:实现了 __set__()
非覆盖描述符:没有实现__set__()
# 发起调用描述器
描述符可以通过其方法名称直接调用。例如, d.__get__(obj) 。
更常见的是在属性访问时自动调用描述符。例如,在中 obj.d 会在 d 的字典中查找 obj 。如果 d 定义了方法 __get__(),则 d.__get__(obj) 根据下面列出的优先级规则进行调用。
调用的细节取决于 obj 是对象还是类。
对于对象来说, object.__getattribute__() 中的机制是将 b.x 转换为 type(b).__dict__['x'].__get__(b, type(b))。
这个实现通过一个优先级链完成,该优先级链赋予数据描述器优先于实例变量的优先级,实例变量优先于非数据描述符的优先级,并如果 __getattr__() 方法存在,为其分配最低的优先级。 完整的C实现可在 Objects/object.c 中的 PyObject_GenericGetAttr()找到。
对于类来说,机制是 type.__getattribute__() 中将 B.x 转换为 B.__dict__['x'].__get__(None, B) 。在纯Python中,它就像:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
要记住的重要点是:
- 描述器由
__getattribute__() 方法调用 - 重写
__getattribute__() 会阻止描述器的自动调用 -
object.__getattribute__() 和type.__getattribute__() 会用不同的方式调用__get__(). - 数据描述符始终会覆盖实例字典。
- 非数据描述器会被实例字典覆盖。
super() 返回的对象还有一个自定义的 __getattribute__() ** ** 方法用来发起调用描述器。 调用 super(B, obj).m() 会搜索 obj.__class__.__mro__ 紧随 B 的基类 A,然后返回 A.__dict__['m'].__get__(obj, B)。 如果其不是描述器,则原样返回 m。 如果不在字典中,m 会转而使用 object.__getattribute__() 进行搜索。
这个实现的具体细节在 Objects/typeobject.c. 的 super_getattro()中,并且你还可以在 Guido's Tutorial 中找到等价的纯Python实现。
以上展示的关于描述器机制的细节嵌入在 object , type , 和 super() 中的 getattribute() 。当类派生自类 object 或有提供类似功能的元类时,它们将继承此机制。同样,类可以通过重写 getattribute() 阻止描述器调用。
# 描述符示例
以下代码创建一个类,其对象是数据描述器,该描述器为每个 get 或 set 打印一条消息。覆盖 getattribute() 是可以对每个属性执行此操作的替代方法。但是,此描述器对于跟踪仅几个选定的属性很有用:
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print('Retrieving', self.name)
return self.val
def __set__(self, obj, val):
print('Updating', self.name)
self.val = val
>>> class MyClass(object):
... x = RevealAccess(10, 'var "x"')
... y = 5
...
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
# 访问顺序
# get
_PyObject_GenericGetAttrWithDict
Python 将照以下优先级逐一确定:
数据描述符:如果类型对象 (含父类) 定义了同名数据描述符属性,属性操作将被其接管;
实现了__set__ 和 get
对象属性:除了①,属性操作默认在属性空间中完成;
非数据描述符:属性访问时,如果①②均不成功,而类型对象 (含父类) 定义了同名非数据描述符,属性访问将被其接管;
仅实现__get__
即数据描述符优先级最高,对象属性空间次之,非数据描述符最低。
举例:
- 对于属性 a ,由于类型对象 t 属性空间定义了数据描述符,将屏蔽实例对象 o 属性空间中的定义;
- 对于属性 b ,由于类型对象 t 属性空间定义的只是非数据描述符,仍以实例对象 o 属性空间定义的为准;
- 对于属性 c ,由于实例对象 o 属性空间未定义,属性访问将以类型对象 t 属性空间定义的非数据描述符为准;
- 对于属性 c ,由于类型对象 t 属性空间定义的只是非数据描述符,属性设置、删除仍以实例对象 o 属性空间为准;
# set
_PyObject_GenericSetAttrWithDict
- 类型中实现了
__set__()方法的描述符则交由__set()__ - 直接在实例属性字典中设置