描述符(descriptor)允许你自定义在引用一个对象的属性时应该完成的事情。 描述符是 Python 中复杂属性访问的基础。它在内部被用于实现 property、方法、类 方法、静态方法和 super 类型。它是一个类,定义了另一个类的属性的访问方式。换句话 说,一个类可以将属性管理委托给另一个类。 描述符类基于 3 个特殊方法,这 3 个方法组成了描述符协议(descriptor protocol): • set(self, obj, type=None):在设置属性时将调用这一方法。在下面的 示例中,我们将其称为 setter。 • get(self, obj, value):在读取属性时将调用这一方法(被称为 getter)。 • delete(self, obj):对属性调用 del 时将调用这一方法。 实现了__get__()和__set__()的描述符被称为数据描述符(data descriptor)。如果只 实现了__get__(),那么就被称为非数据描述符(non-data descriptor)。 在每次属性查找中,这个协议的方法实际上由对象的特殊方法__getattribute__() 调用(不要与__getattr__()弄混,后者用于其他目的)。每次通过点号(形式为 instance.attribute)或者 getattr(instance, 'attribute')函数调用来执行 这样的查找时,都会隐式地调用__getattribute__(),它按下列顺序查找该属性: 1.验证该属性是否为实例的类对象的数据描述符。 2.如果不是,就查看该属性是否能在实例对象的__dict__中找到。 3.最后,查看该属性是否为实例的类对象的非数据描述符。 换句话说,数据描述符优先于__dict__查找,而__dict__查找优先于非数据描述符。 为了表达得更清楚,下面是 Python 官方文档中的示例,给出了描述符在真实代码中的 工作方式: class RevealAccess(object): """一个数据描述符,正常设定值并返回值,同时打印出记录访问的信息。

"""

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__()方法并返回它的值,每次对这个属性赋值时都会调用 set()。虽然前一个例子没有给出描述符__del__方法的例子,但现在也应该清楚了: 每次通过 del instance.attribute 语句或 delattr(instance, 'attribute') 调用删除一个实例属性时都会调用它。 由于上述原因,数据描述符和非数据描述符的区别很重要。Python 已经使用描述符协 议将类函数绑定为实例方法。它还支持了 classmethod 和 staticmethod 装饰器背后 的机制。事实上,这是因为函数对象也是非数据描述符,如下所示: def function(): pass hasattr(function, ' __get __')

True

hasattr(function, ' __set __') False 对于 lambda 表达式创建的函数也是如此: hasattr(lambda: None, ' __get __') True hasattr(lambda: None, ' __set __') False 因此,如果没有__dict__优先于非数据描述符,我们将不可能在运行时在已经构建好 的实例上动态覆写特定的方法。幸运的是,多亏了 Python 描述符的工作方式。由于这一工 作方法,使得开发人员可以使用一种叫作猴子补丁(monkey-patching)的流行技术来改变 实例的工作方式,而不需要子类化。 现实例子 — 延迟求值属性 描述符的一个示例用法就是将类属性的初始化延迟到被实例访问时。如果这些属性的初始 化依赖全局应用上下文的话,那么这一点可能有用。另一个使用场景是初始化的代价很大,但 在导入类的时候不知道是否会用到这个属性。这样的描述符可以按照如下所示来实现: class InitOnAccess: def init(self, klass, *args, **kwargs): self.klass = klass self.args = args self.kwargs = kwargs self._initialized = None def get(self, instance, owner): if self._initialized is None: print('initialized!') self._initialized = self.klass(*self.args, **self.kwargs) else: print('cached!') return self.initialized 下面是示例用法: class MyClass: ... lazily _initialized = InitOnAccess(list, "argument") ... m = MyClass()

m.lazily_ initialized initialized! ['a', 'r', 'g', 'u', 'm', 'e', 'n', 't'] m.lazily initialized cached! ['a', 'r', 'g', 'u', 'm', 'e', 'n', 't'] PyPI上OpenGL的官方Python库PyOpenGL用到了相似的技术来实现lazy_property, 它既是装饰器又是数据描述符,如下所示: class lazy_property(object): def init(self, function): self.fget = function def get(self, obj, cls): value = self.fget(obj) setattr(obj, self.fget.name, value) return value 这样的实现与使用 property 装饰器(稍后介绍)类似,但它所包装的函数仅执行一 次,然后类属性就被替换为它的返回值。当开发人员需要同时满足以下两点要求时,这种 技术通常很有用。 • 对象实例需要被保存为实例之间共享的类属性,以节约资源。 • 在全局导入时对象不能被初始化,因为其创建过程依赖某个全局应用状态/上下文。 对于使用 OpenGL 编写的应用来说,往往需要同时满足这两点要求。举个例子,在 OpenGL 中创建着色器的代价非常高,因为需要对 GLSL(OpenGL 着色语言)编写的代 码进行编译。合理的做法是只创建一次,然后将其定义放在需要用到它的类附近。另一方 面,如果没有对 OpenGL 上下文进行初始化,是无法执行着色器编译的,因此很难在全局 导入时在全局模块命名空间中可靠地定义并编译着色器。 下面的例子展示了 PyOpenGL 的 lazy_property 装饰器(这里是 lazy_class attribute)的修改版在某个虚构的基于 OpenGL 应用中的可能用法。为了在不同的类实例 之间共享属性,需要将加粗部分的代码修改为原始的 lazy_property 装饰器,如下所示: import OpenGL.GL as gl from OpenGL.GL import shaders class lazy_class_attribute(object): def init(self, function): self.fget = function

def __get __(self, obj, cls): value = self.fget(obj or cls)

注意:无论是类级别还是实例级别的访问

都要保存在类对象中,而不是保存在实例中

setattr(cls, self.fget. __name __, value) return value class ObjectUsingShaderProgram(object):

trivial pass-through vertex shader implementation

VERTEX_CODE = """ #version 330 core layout(location = 0) in vec4 vertexPosition; void main(){ gl_Position = vertexPosition; } """

trivial fragment shader that results in everything

drawn with white color

FRAGMENT_CODE = """ #version 330 core out lowp vec4 out_color; void main(){ out_color = vec4(1, 1, 1, 1); } """ @lazy_class_attribute def shader_program(self): print("compiling!") return shaders.compileProgram( shaders.compileShader( self.VERTEX_CODE, gl.GL_VERTEX_SHADER ), shaders.compileShader( self.FRAGMENT_CODE, gl.GL_FRAGMENT_SHADER ) ) 和所有 Python 高级语法特性一样,这一特性也应该谨慎使用,并在代码中详细说明。 对于没有经验的开发者而言,这种类行为的改变可能令人既困惑又意外,因为描述符影响 的是类行为最基本的内容(例如属性访问)。因此,如果描述符在项目代码库中发挥重要作 用的话,那么确保团队所有成员都熟悉并理解这一概念是很重要的。