详解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中的自定义函数学习笔记
Sep 23 Python
Python中的列表生成式与生成器学习教程
Mar 13 Python
基于numpy.random.randn()与rand()的区别详解
Apr 17 Python
使用python画个小猪佩奇的示例代码
Jun 06 Python
tensorflow学习教程之文本分类详析
Aug 07 Python
对python For 循环的三种遍历方式解析
Feb 01 Python
Python学习笔记之Zip和Enumerate用法实例分析
Aug 14 Python
python 利用已有Ner模型进行数据清洗合并代码
Dec 24 Python
Python实现Word文档转换Markdown的示例
Dec 22 Python
Python+Appium新手教程
Apr 17 Python
Python中glob库实现文件名的匹配
Jun 18 Python
Python作用域和名称空间的详细介绍
Apr 13 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 file_get_contents 函数超时的几种解决方法
2009/07/30 PHP
mysql,mysqli,PDO的各自不同介绍
2012/09/19 PHP
深入php-fpm的两种进程管理模式详解
2013/06/03 PHP
一个比较不错的PHP日历类分享
2014/11/18 PHP
php创建session的方法实例详解
2015/01/27 PHP
PHP批量去除BOM头内容信息代码
2016/03/11 PHP
解决php 处理 form 表单提交多个 name 属性值相同的 input 标签问题
2017/05/11 PHP
PHP实现基于栈的后缀表达式求值功能
2017/11/10 PHP
PHP实现微信提现功能(微信商城)
2019/11/21 PHP
创建js对象和js类的方法汇总
2014/12/24 Javascript
angularJS 中$attrs方法使用指南
2015/02/09 Javascript
详解AngularJS的通信机制
2015/06/18 Javascript
JS中常用的正则表达式
2016/09/29 Javascript
根据输入邮箱号跳转到相应登录地址的解决方法
2016/12/13 Javascript
解决Vue.js 2.0 有时双向绑定img src属性失败的问题
2018/03/14 Javascript
Electron-vue开发的客户端支付收款工具的实现
2019/05/24 Javascript
原生JS实现相邻月份日历
2020/10/13 Javascript
使用Python神器对付12306变态验证码
2016/01/05 Python
Python中的条件判断语句与循环语句用法小结
2016/03/21 Python
Windows和Linux下Python输出彩色文字的方法教程
2017/05/02 Python
django模板语法学习之include示例详解
2017/12/17 Python
selenium在执行phantomjs的API并获取执行结果的方法
2018/12/17 Python
python选取特定列 pandas iloc,loc,icol的使用详解(列切片及行切片)
2019/08/06 Python
详解Python3 pickle模块用法
2019/09/16 Python
python 队列基本定义与使用方法【初始化、赋值、判断等】
2019/10/24 Python
python实现贪吃蛇游戏源码
2020/03/21 Python
美国网上鞋子零售商:Dr. Scholl’s Shoes
2017/11/17 全球购物
孤独星球出版物:Lonely Planet Publications
2018/03/17 全球购物
丝芙兰波兰:Sephora.pl
2018/03/25 全球购物
商业房地产广告语
2014/03/13 职场文书
教师民族团结演讲稿
2014/08/27 职场文书
2014年班干部工作总结
2014/11/25 职场文书
2015年财务试用期工作总结
2014/12/24 职场文书
面试复试通知单
2015/04/24 职场文书
未婚证明格式
2015/06/15 职场文书
spring boot实现文件上传
2022/08/14 Java/Android