深入解析Python中的descriptor描述器的作用及用法
一般来说,一个描述器是一个有“绑定行为”的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。有这些方法的对象叫做描述器。
默认对属性的访问控制是从对象的字典里面(__dict__)中获取(get), 设置(set)和删除(delete)它。举例来说, a.x 的查找顺序是, a.__dict__['x'] , 然后 type(a).__dict__['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)).如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)
描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和 super 的背后的实现机制。描述器在Python自身中广泛使用,以实现Python 2.2中引入的新式类。描述器简化了底层的C代码,并为Python的日常编程提供了一套灵活的新工具。
描述器协议
descr.__get__(self, obj, type=None) --> value descr.__get__(self, obj, value) --> None descr.__delete__(self, obj) --> None
一个对象如果是一个描述器,被当做对象属性(很重要)时重写默认的查找行为。
如果一个对象同时定义了__get__和__set__,它叫data descriptor。仅定义了__get__的描述器叫non-data descriptor。
data descriptor和non-data descriptor区别在于: 相对于实例的字典的优先级,如果实例字典有与描述器具同名的属性,如果描述器是data descriptor,优先使用data descriptor。如果是non-data descriptor,优先使用字典中的属性。
class B(object): def __init__(self): self.name = 'mink' def __get__(self, obj, objtype=None): return self.name class A(object): name = B() a = A() print a.__dict__ # print {} print a.name # print mink a.name = 'kk' print a.__dict__ # print {'name': 'kk'} print a.name # print kk
这里B是一个non-data descriptor所以当a.name = 'kk'的时候,a.__dict__里会有name属性, 接下来给它设置__set__
def __set__(self, obj, value): self.name = value ... do something a = A() print a.__dict__ # print {} print a.name # print mink a.name = 'kk' print a.__dict__ # print {} print a.name # print kk
因为data descriptor访问属性优先级比实例的字典高,所以a.__dict__是空的。
描述器的调用
描述器可以直接这么调用: d.__get__(obj)
然而更常见的情况是描述器在属性访问时被自动调用。举例来说, obj.d 会在 obj 的字典中找 d ,如果 d 定义了 __get__ 方法,那么 d.__get__(obj) 会依据下面的优先规则被调用。
调用的细节取决于 obj 是一个类还是一个实例。另外,描述器只对于新式对象和新式类才起作用。继承于 object 的类叫做新式类。
对于对象来讲,方法 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__() 方法会阻止正常的描述器调用
- __getattribute__() 只对新式类的实例可用
- object.__getattribute__() 和 type.__getattribute__() 对 __get__() 的调用不一样
- 资料描述器总是比实例字典优先。
- 非资料描述器可能被实例字典重写。(非资料描述器不如实例字典优先)
- super() 返回的对象同样有一个定制的 __getattribute__() 方法用来调用描述器。调用 super(B, obj).m() 时会先在 obj.__class__.__mro__ 中查找与B紧邻的基类A,然后返回 A.__dict__['m'].__get__(obj, A) 。如果不是描述器,原样返回 m 。如果实例字典中找不到 m ,会回溯继续调用 object.__getattribute__() 查找。(译者注:即在 __mro__ 中的下一个基类中查找)
注意:在Python 2.2中,如果 m 是一个描述器, super(B, obj).m() 只会调用方法 __get__() 。在Python 2.3中,非资料描述器(除非是个旧式类)也会被调用。 super_getattro() 的实现细节在: Objects/typeobject.c ,[del] 一个等价的Python实现在 Guido's Tutorial [/del] (译者注:原文此句已删除,保留供大家参考)。
以上展示了描述器的机理是在 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
这个协议非常简单,并且提供了令人激动的可能。一些用途实在是太普遍以致于它们被打包成独立的函数。像属性(property), 方法(bound和unbound method), 静态方法和类方法都是基于描述器协议的。

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

PHP主要是过程式编程,但也支持面向对象编程(OOP);Python支持多种范式,包括OOP、函数式和过程式编程。PHP适合web开发,Python适用于多种应用,如数据分析和机器学习。

PHP适合网页开发和快速原型开发,Python适用于数据科学和机器学习。1.PHP用于动态网页开发,语法简单,适合快速开发。2.Python语法简洁,适用于多领域,库生态系统强大。

在 Sublime Text 中运行 Python 代码,需先安装 Python 插件,再创建 .py 文件并编写代码,最后按 Ctrl B 运行代码,输出会在控制台中显示。

PHP起源于1994年,由RasmusLerdorf开发,最初用于跟踪网站访问者,逐渐演变为服务器端脚本语言,广泛应用于网页开发。Python由GuidovanRossum于1980年代末开发,1991年首次发布,强调代码可读性和简洁性,适用于科学计算、数据分析等领域。

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

Golang在性能和可扩展性方面优于Python。1)Golang的编译型特性和高效并发模型使其在高并发场景下表现出色。2)Python作为解释型语言,执行速度较慢,但通过工具如Cython可优化性能。

在 Visual Studio Code(VSCode)中编写代码简单易行,只需安装 VSCode、创建项目、选择语言、创建文件、编写代码、保存并运行即可。VSCode 的优点包括跨平台、免费开源、强大功能、扩展丰富,以及轻量快速。

在 Notepad 中运行 Python 代码需要安装 Python 可执行文件和 NppExec 插件。安装 Python 并为其添加 PATH 后,在 NppExec 插件中配置命令为“python”、参数为“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通过快捷键“F6”运行 Python 代码。
