深入解析Python中的descriptor描述器的作用及用法


Posted in Python onJune 27, 2016

一般来说,一个描述器是一个有“绑定行为”的对象属性(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), 静态方法和类方法都是基于描述器协议的。

Python 相关文章推荐
Python开发的单词频率统计工具wordsworth使用方法
Jun 25 Python
Django中Forms的使用代码解析
Feb 10 Python
Python多线程编程之多线程加锁操作示例
Sep 06 Python
Python如何获得百度统计API的数据并发送邮件示例代码
Jan 27 Python
Python 通过微信控制实现app定位发送到个人服务器再转发微信服务器接收位置信息
Aug 05 Python
利用ImageAI库只需几行python代码实现目标检测
Aug 09 Python
python字符串下标与切片及使用方法
Feb 13 Python
Python3 selenium 实现QQ群接龙自动化功能
Apr 17 Python
django下创建多个app并设置urls方法
Aug 02 Python
python logging 重复写日志问题解决办法详解
Aug 04 Python
Sublime Text3最新激活注册码分享适用2020最新版 亲测可用
Nov 12 Python
Python读取ini配置文件传参的简单示例
Jan 05 Python
Python中的字符串查找操作方法总结
Jun 27 #Python
解析Python中的__getitem__专有方法
Jun 27 #Python
详解Python中的__getitem__方法与slice对象的切片操作
Jun 27 #Python
Python使用smtplib模块发送电子邮件的流程详解
Jun 27 #Python
Python教程之全局变量用法
Jun 27 #Python
python设计模式大全
Jun 27 #Python
Python处理JSON时的值报错及编码报错的两则解决实录
Jun 26 #Python
You might like
php简单定时执行任务的实现方法
2015/02/23 PHP
比较简单的异步加载JS文件的代码
2009/07/18 Javascript
体验js中splice()的强大(插入、删除或替换数组的元素)
2013/01/16 Javascript
jquery插件实现鼠标经过图片右侧显示大图的效果(类似淘宝)
2013/02/04 Javascript
JavaScript电子时钟倒计时第二款
2016/01/10 Javascript
web前端开发upload上传头像js示例代码
2016/10/22 Javascript
Javascript 实现全屏滚动实例代码
2016/12/31 Javascript
老生常谈javascript的面向对象思想
2017/08/22 Javascript
JS使用tofixed与round处理数据四舍五入的区别
2017/10/25 Javascript
详解Angular结合zTree异步加载节点数据
2018/01/20 Javascript
vue自定义全局共用函数详解
2018/09/18 Javascript
Vue项目服务器部署之子目录部署方法
2019/05/12 Javascript
axios如何利用promise无痛刷新token的实现方法
2019/08/27 Javascript
vue中如何实现后台管理系统的权限控制的方法步骤
2019/09/05 Javascript
[01:29:31]VP VS VG Supermajor小组赛胜者组第二轮 BO3第一场 6.2
2018/06/03 DOTA
Python实现的批量下载RFC文档
2015/03/10 Python
python简单实现基于SSL的IRC bot实例
2015/06/15 Python
如何在Python函数执行前后增加额外的行为
2016/10/20 Python
Python IDLE清空窗口的实例
2018/06/25 Python
对python读取CT医学图像的实例详解
2019/01/24 Python
Python使用crontab模块设置和清除定时任务操作详解
2019/04/09 Python
python“静态”变量、实例变量与本地变量的声明示例
2020/11/13 Python
html5的新增的标签和废除的标签简要概述
2013/02/20 HTML / CSS
Kusmi茶美国官网:优质散叶茶和茶包
2019/10/13 全球购物
.NET是怎么支持多种语言的
2015/02/24 面试题
季度思想汇报
2014/01/01 职场文书
应届大学生简历中的自我评价
2014/01/15 职场文书
车队司机自我鉴定
2014/03/02 职场文书
白岩松演讲
2014/05/21 职场文书
大学生村官座谈会发言材料
2014/05/25 职场文书
会员卡清退活动总结
2014/08/27 职场文书
幼儿园三八妇女节活动总结
2015/02/06 职场文书
寒假安全保证书
2015/02/28 职场文书
怎样写好演讲稿题目?
2019/08/21 职场文书
Java Optional<Foo>转换成List<Bar>的实例方法
2021/06/20 Java/Android
VS2019连接MySQL数据库的过程及常见问题总结
2021/11/27 MySQL