详解Python中的Descriptor描述符类


Posted in Python onJune 14, 2016

描述符是调和属性访问的一个类。描述符类可用来获取、设置或删除属性值。描述符对象是在类定义的时候构建在一个类中的。

一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、 __set__()和__delete__(),一个对象中只要包含了这三个方法(译者注:包含至少一个),就称它为描述符。
属性访问的默认行为是从一个对象的字典中获取 (get)、设置 (set)、删除 (delete) 属性。例如:a.x 的查找链始于 a.__dict__['x'],然后是 type(a).__dict__['x'],然后是 type(a) 除元类之外的基类(译者注:如果继承树很深,可能会访问多个基类)。如果查找到的值是包含一个描述符方法的对象,那么Python可能会重写(该对象)的默认行为并调用那个描述符方法。注意只有在新式对象或者新式类(继承自object或者type)中描述符才会被调用。
描述符是一个功能强大、通用的协议。它们是属性、方法、静态方法、类方法、super()背后的实现机制。它们被广泛使用于Python 2.2中用来实现新式类。描述符简化了底层的C代码并为Python编程提供了一套灵活的新工具。

描述符设计模式有两个部分:一个所有者类和属性描述符本身。所有者类给它的属性使用一个或多个描述符。描述符类定义了获取、设置和删除方法的组合。描述符类的一个实例将会是所有者类的一个属性。

特性是基于所有者类的方法函数。描述符不像特性,是一个类的实例,与所有者类不同。因此,描述符通常是可重用的通用属性。所有者类可以有多个不同描述符类的实例来类管理具有相似行为的属性。

不像其他属性,描述符在类级别上创建。它们不是在__init()__初始化时创建。然而描述符的值可以在初始化期间设置,描述符通常是作为类的一部分,在任何方法函数之外来构建的。

当所有者类被定义时,每个描述符对象都是被绑定到一个不同的类级别属性的描述符类实例。

被确认为一个描述符,一个类必须实现以下三个方法的任意组合。

  • Descriptor.__get__(self, instance, owner) -> object:在这个方法中,instance参数是即将被访问的对象的self变量。owner参数是所有者类的对象。如果这个描述符在类的上下文中被调用,instance参数将得到一个None值。这必须返回描述符的值。
  • Descriptor.__set__(self, instance, value):在这个方法中,instance参数是即将被访问的对象的self变量。value参数是描述符需要设置的新值。
  • Descriptor.__delete__(self, instance)在这个方法中,instance参数是即将被访问的对象的self变量。该描述符的方法必须删除这个属性的值。

有时,一个描述符类还将需要一个__init__()方法函数来初始化描述符的内部状态。

有两种基于已定义方法的描述符,如下所示:

1.非数据描述符:这种描述符定义__set__()或__delete__()或两者皆有。它不能定义__get__()。非数据描述符对象往往会被用作表达式的一部分。它可能是一个可调用对象,或者它可能有自己的属性或方法。一个不可变的非数据描述符必须实现__set__(),但可能只是抛出AttributeError。这些描述符设计时很简单,因为接口更灵活。
2.数据描述符:这种描述符至少定义__get__()。通常,它定义__get__()和__set__()来创建一个可变对象。鉴于描述符将在很大程度上是不可见的,则不能更进一步的再定义属性或方法。属性的引用有一个数据描述符的数据被委托给描述符的__get__()、__set__()或__delete__()方法。这些是很难设计的,所以我们稍后来再看。
描述符有各种各样的用例。在内部,Python使用描述符有以下几个原因:

  • 在隐藏的内部,类的方法是作为描述符来实现。这些非数据描述符应用方法函数到对象以及不同的参数值。
  • property()函数通过给一个字段创建数据描述符来实现。
  • 一个类方法或静态方法被实现为一个描述符;这被应用到类中来代替类的实例。

当我们考虑一个描述符的目的,我们还必须为数据作为描述符可以正常工作来考察三种常见用例,如下所示:

  • 描述符对象有数据或获取到了数据。在这种情况下,描述符对象的self变量是有意义的且描述符是有状态的。数据描述符的__get__()方法返回这个内部数据。非数据描述符,描述符有其他方法或属性来访问这些数据。
  • 包含数据的所有者实例。在这种情况下,描述符对象必须使用instance参数来引用值到所有者对象中。数据描述符的__get__()方法从实例获取数据。非数据描述符有其他方法访问实例数据。
  • 包含相关数据的所有者类。在这种情况下,描述符对象必须使用owner参数。这是常用的当描述符实现了应用于整个类的静态方法或类方法。

我们将仔细看下第一种情况。我们看看创建带有__get__()和__set__()方法的数据描述符。我们也会看看创建没有__get__()方法的非数据描述符。

第二种情况(所有者实例中的数据)展示了@property装饰器都做了些什么。可能的优势是描述符有一个传统的特性将计算从拥有者类移到描述符类中。这倾向于分片类设计且可能不是最好的方法。如果计算是真正史诗般的复杂,策略模式可能会更好。

第三种情况展示@staticmethod和@classmethod装饰器是如何实现的。我们不需要重新发明轮子。

1、使用非数据描述符
我们经常会有一些紧密绑定了属性值的小对象。对于这个示例,我们将看看数值被绑定到单位的举措。

下面是一个简单的非数据描述符类,它缺少一个__get__()方法:

class UnitValue_1:
  """Measure and Unit combined."""
  def __init__(self, unit):
    self.value = None
    self.unit = unit
    self.default_format = "5.2f"

  def __set__(self, instance, value):
    self.value = value

  def __str__(self):
    return "{value:{spec}} {unit}"
    .format(spec=self.default_format, **self.__dict__)

  def __format__(self, spec="5.2f"):
    #print( "formatting", spec )
    if spec == "":
      spec = self.default_format
    return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__)

这个类定义了一对简单的值,一个可变的(值),另一个是有效的不可变对象(单位)。

当这个描述符被访问时,描述符对象本身是可用的,且描述符的其他方法或属性可以被使用。我们可以使用这个描述符来创建类去管理尺寸和其他与物理单位有关的数值。

下面是一个类,做速度-时间-距离的及早计算:

class RTD_1:
  rate = UnitValue_1("kt")
  time = UnitValue_1("hr")
  distance = UnitValue_1("nm")
  def __init__(self, rate=None, time=None, distance=None):
    if rate is None:
      self.time = time
      self.distance = distance
      self.rate = distance / time
    if time is None:
      self.rate = rate
      self.distance = distance
      self.time = distance / rate
    if distance is None:
      self.rate = rate
      self.time = time
      self.distance = rate * time

  def __str__(self):
    return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self)

一旦对象被创建且属性被加载,丢失的值就已经被计算。一旦计算,描述符可以检查获取值或单位的名称。此外,描述符对str()有一个方便的响应和请求格式。

下面是描述符和RTD_1类之间的交互:

>>> m1 = RTD_1(rate=5.8, distance=12)
>>> str(m1)
'rate: 5.80 kt time: 2.07 hr distance: 12.00 nm'
>>> print("Time:", m1.time.value, m1.time.unit)
Time: 2.0689655172413794 hr

我们创建了一个带有rate和distance参数的RTD_1实例。这些都是用来计算rate和distance描述符的__set__()方法。

当我们请求str(m1),这会计算RTD_1的所有str()方法,转而使用rate、time和distance描述符的__format__()方法。这为我们提供了数字和单位。

鉴于非数据描述符没有__get__()且不返回其内部值,我们可以访问描述符的单个元素。

2、使用数据描述符
数据描述符设计要复杂一些,因为它对接口有限制。它必须有一个__get__()方法,且只能有__set__()或__delete__()。这是所有的接口:这些方法从一到三,没有其他方法。引入一个额外的方法意味着Python不会把该类当作一个正确的数据描述符。

我们会使用描述符设计一个简单的单位转换模式,可以在__get__()和__set__()方法做适当的转换。

下面是一个单位描述符的超类,它在其他单位和标准单位之间做转换:

class Unit:
  conversion = 1.0
  def __get__(self, instance, owner):
    return instance.kph * self.conversion

  def __set__(self, instance, value):
    instance.kph = value / self.conversion

该类用简单的乘法和除法将标准单位转换为其他非标准单位,反之亦然。

通过这个超类,我们可以从一个标准单位定义一些转换。在前面的示例,标准单位是千米时(公里/小时)。

以下是这两个转换描述符

class Knots(Unit):
  conversion = 0.5399568

class MPH(Unit):
  conversion = 0.62137119

继承方法非常有用。唯一改变的是转换因子。这些类可用于处理涉及单位转换的值。我们可以处理英里每小时或可交换的节点。下面是一个标准单位的单位描述符,公里每小时:

class KPH(Unit):

  def __get__(self, instance, owner):
    return instance._kph

  def __set__(self, instance, value):
    instance._kph = value

这个类代表一个标准,所以不做任何转换。它使用一个私有变量实例保存速度千米每小时的标准值。避免任何算术转换是一个简单的技术优化。避免任何一个公共字段的引用是至关重要的,来规避无限递归。

下面这个类,它对于一个给定的尺寸提供了一组转换:

class Measurement:
  kph = KPH()
  knots = Knots()
  mph = MPH()
  def __init__(self, kph=None, mph=None, knots=None):
    if kph:
      self.kph = kph
    elif mph:
      self.mph = mph
    elif knots:
      self.knots = knots
    else:
      raise TypeError

  def __str__(self):
    return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self)

对于不同的单位每个类级别的属性都是描述符。各种描述符的获取和设置方法会做适当的转换。我们可以使用这个类在各种单位之间进行速度转换。

以下是与Measurement类交互的一个例子:

>>> m2 = Measurement(knots=5.9)
>>> str(m2)
'rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots'
>>> m2.kph
10.92680006993152
>>> m2.mph
6.789598762345432

我们通过设置不同的描述符创建了一个Measurement类的对象。在第一个示例中,我们设置了节点描述符。

当我们显示的值是一个大字符串,则每个描述符的__get__()都将被使用。这些方法从所有者对象获取内部kph字段值,应用一个转换因子,且返回一个结果值。

kph字段还使用了一个描述符。这个描述符不做任何转换;然而,它只是返回了缓存在所有者对象的私有值。KPH和Knots描述符要求所有者类实现一个kph属性。

Python 相关文章推荐
Python生成随机MAC地址
Mar 10 Python
Django与遗留的数据库整合的方法指南
Jul 24 Python
python中的错误处理
Apr 10 Python
《Python学习手册》学习总结
Jan 17 Python
TensorFlow 实战之实现卷积神经网络的实例讲解
Feb 26 Python
python2.7 json 转换日期的处理的示例
Mar 07 Python
python爬取个性签名的方法
Jun 17 Python
python程序封装为win32服务的方法
Mar 07 Python
python进程池实现的多进程文件夹copy器完整示例
Nov 27 Python
解决tensorflow添加ptb库的问题
Feb 10 Python
Python数据可视化处理库PyEcharts柱状图,饼图,线性图,词云图常用实例详解
Feb 10 Python
python GUI库图形界面开发之PyQt5滚动条控件QScrollBar详细使用方法与实例
Mar 06 Python
浅析Python中的getattr(),setattr(),delattr(),hasattr()
Jun 14 #Python
Python中getattr函数和hasattr函数作用详解
Jun 14 #Python
Python模块包中__init__.py文件功能分析
Jun 14 #Python
Python计算字符宽度的方法
Jun 14 #Python
Python中文分词实现方法(安装pymmseg)
Jun 14 #Python
Python找出list中最常出现元素的方法
Jun 14 #Python
Python中列表元素转为数字的方法分析
Jun 14 #Python
You might like
php include类文件超时问题处理
2015/02/06 PHP
PHP开发api接口安全验证操作实例详解
2020/03/26 PHP
JSON 学习之完全手册 图文
2007/05/29 Javascript
ImageZoom 图片放大镜效果(多功能扩展篇)
2010/04/14 Javascript
Chrome中JSON.parse的特殊实现
2011/01/12 Javascript
非主流的textarea自增长实现js代码
2011/12/20 Javascript
javascript实现TreeView 无刷新展开的实例代码
2013/07/13 Javascript
jQuery学习之prop和attr的区别示例介绍
2013/11/15 Javascript
JS兼容浏览器的导出Excel(CSV)文件的方法
2014/05/03 Javascript
jQuery 获取/设置/删除DOM元素的属性以a元素为例
2014/05/23 Javascript
JS+CSS实现仿新浪微博搜索框的方法
2015/02/24 Javascript
jquery Easyui快速开发总结
2015/08/20 Javascript
js实现创建删除html元素小结
2015/09/30 Javascript
JavaScript实现垂直滚动条效果
2017/01/18 Javascript
结合mint-ui移动端下拉加载实践方法总结
2017/11/08 Javascript
微信小程序实现折叠面板
2018/01/31 Javascript
js中document.write和document.writeln的区别
2018/03/11 Javascript
Vue实现table上下移动功能示例
2019/02/21 Javascript
JavaScript复制变量三种方法实例详解
2020/01/09 Javascript
使用Angular9和TypeScript开发RPG游戏的方法
2020/03/25 Javascript
python通过apply使用元祖和列表调用函数实例
2015/05/26 Python
Python中断言Assertion的一些改进方案
2016/10/27 Python
Pandas 数据框增、删、改、查、去重、抽样基本操作方法
2018/04/12 Python
解决Keras 与 Tensorflow 版本之间的兼容性问题
2020/02/07 Python
基于python3实现倒叙字符串
2020/02/18 Python
Python3 webservice接口测试代码详解
2020/06/23 Python
如何使用pycharm连接Databricks的步骤详解
2020/09/23 Python
哈萨克斯坦移动和数字技术在线商店:SatelOnline.kz
2020/09/04 全球购物
商业项目策划方案
2014/06/05 职场文书
语文教研活动总结
2014/07/02 职场文书
乡镇党委书记个人整改措施
2014/09/15 职场文书
小学教师师德师风个人整改措施
2014/09/18 职场文书
学生检讨书范文
2015/01/27 职场文书
个人求职意向书
2015/05/11 职场文书
董事长助理工作总结2015
2015/07/23 职场文书
Python Flask请求扩展与中间件相关知识总结
2021/06/11 Python