【LemonCK】描述器使用指南【转】
摘要
定义描述器,总结描述器协议,展示描述器被如何使用。测试一个自定义的描述器和若干 Python 内置的描述器,包括函数、属性、静态方法和类方法。通过给出一个纯 Python 的等价实现和例程,展示每个描述器如何工作。
学习描述器不仅能提供接触到更多工具集的途径,还能更深地理解 Python 工作的原理并更加体会到其设计的优雅性。
定义和简介
一般地,一个描述器是一个包含 “绑定行为” 的对象,对其属性的访问被描述器协议中定义的方法覆盖。这些方法有:__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
以上就是全部。定义这些方法中的任何一个的对象被视为描述器,并在被作为属性时覆盖其默认行为。
如果一个对象定义了 __set__()
或 __delete__()
,则它会被视为数据描述器。 仅定义了 __get__()
的描述器称为非数据描述器(它们通常被用于方法,但也可以有其他用途)。
数据和非数据描述器的不同之处在于,如何计算实例字典中条目的替代值。如果实例的字典具有与数据描述器同名的条目,则数据描述器优先。如果实例的字典具有与非数据描述器同名的条目,则该字典条目优先。
为了使数据描述器成为只读的,应该同时定义 __get__()
和 __set__()
,并在 __set__()
中引发 AttributeError
。用引发异常的占位符定义 __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__()
. -
数据描述器始终会覆盖实例字典。
-
非数据描述器会被实例字典覆盖。
The object returned by super()
also has a custom __getattribute__()
method for invoking descriptors. The attribute lookup super(B, obj).m
searches obj.__class__.__mro__
for the base class A
immediately following B
and then returns A.__dict__['m'].__get__(obj, B)
. If not a descriptor, m
is returned unchanged. If not in the dictionary, m
reverts to a search using 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
这个协议很简单,并提供了令人兴奋的可能性。有几种用例非常普遍,以至于它们被打包到单独的函数调用中。属性、绑定方法、静态方法和类方法均基于描述器协议。