详解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 selenium 三种等待方式详解(必会)
Sep 15 Python
Python在信息学竞赛中的运用及Python的基本用法(详解)
Aug 15 Python
从CentOS安装完成到生成词云python的实例
Dec 01 Python
详解Python判定IP地址合法性的三种方法
Mar 06 Python
python线程池threadpool实现篇
Apr 27 Python
用TensorFlow实现lasso回归和岭回归算法的示例
May 02 Python
Django框架首页和登录页分离操作示例
May 28 Python
详解python和matlab的优势与区别
Jun 28 Python
python openpyxl使用方法详解
Jul 18 Python
Pycharm 字体大小调整设置的方法实现
Sep 27 Python
pytorch实现MNIST手写体识别
Feb 14 Python
Python基于Socket实现简单聊天室
Feb 17 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
咖啡知识 除了喝咖啡还有那些知识点
2021/03/06 新手入门
mysql 查询指定日期时间内sql语句实现原理与代码
2012/12/16 PHP
在yii中新增一个用户验证的方法详解
2013/06/20 PHP
php中Y2K38的漏洞解决方法实例分析
2014/09/22 PHP
Zend Framework入门知识点小结
2016/03/19 PHP
php面向对象编程self和static的区别
2016/05/08 PHP
mysql查找删除重复数据并只保留一条实例详解
2016/09/24 PHP
利用javascript实现禁用网页上所有文本框,下拉菜单,多行文本域
2013/12/14 Javascript
javascript window.open打开新窗口后无法再次打开该窗口问题的解决方法
2014/04/12 Javascript
JQuery工具函数汇总
2015/06/15 Javascript
浅谈JS正则表达式的RegExp对象和括号的使用
2016/07/28 Javascript
jQuery DOM节点的遍历方法小结
2017/08/15 jQuery
jQuery使用动画队列自定义动画操作示例
2018/06/16 jQuery
详解JS中统计函数执行次数与执行时间
2018/09/04 Javascript
React 组件中的 bind(this)示例代码
2018/09/16 Javascript
jQuery实现ajax的嵌套请求案例分析
2019/02/16 jQuery
Python中的index()方法使用教程
2015/05/18 Python
理解python正则表达式
2016/01/15 Python
django中的HTML控件及参数传递方法
2018/03/20 Python
在PyCharm导航区中打开多个Project的关闭方法
2019/01/17 Python
在Python 字典中一键对应多个值的实例
2019/02/03 Python
Django网络框架之HelloDjango项目创建教程
2019/06/06 Python
python用WxPython库实现无边框窗体和透明窗体实现方法详解
2020/02/21 Python
Python基于百度AI实现OCR文字识别
2020/04/02 Python
python爬虫学习笔记之pyquery模块基本用法详解
2020/04/09 Python
Linux安装Python3如何和系统自带的Python2并存
2020/07/23 Python
python实现马丁策略的实例详解
2021/01/15 Python
应届生程序员求职信
2013/11/05 职场文书
大学社团活动策划书
2014/01/26 职场文书
婚礼主持词
2014/03/13 职场文书
工地标语大全
2014/06/18 职场文书
村主任“四风”问题个人整改措施
2014/10/04 职场文书
家庭财产分割协议范文
2014/11/24 职场文书
婚礼女方父母答谢词
2015/01/04 职场文书
详解MongoDB的条件查询和排序
2021/06/23 MongoDB
Win10 heic文件怎么打开 ? Win10 heic文件打开教程
2022/04/06 数码科技