详解Python中的Descriptor描述符类
描述符是调和属性访问的一个类。描述符类可用来获取、设置或删除属性值。描述符对象是在类定义的时候构建在一个类中的。 一般来说,描述符是一个具有绑定行为的对象属性,其属性的访问被描述符协议方法覆写。这些方法是__get__()、 __set__()和__delete__(),一个对象中只要包含了这三个方法(译者注:包含至少一个),就称它为描述符。 描述符设计模式有两个部分:一个所有者类和属性描述符本身。所有者类给它的属性使用一个或多个描述符。描述符类定义了获取、设置和删除方法的组合。描述符类的一个实例将会是所有者类的一个属性。 特性是基于所有者类的方法函数。描述符不像特性,是一个类的实例,与所有者类不同。因此,描述符通常是可重用的通用属性。所有者类可以有多个不同描述符类的实例来类管理具有相似行为的属性。 不像其他属性,描述符在类级别上创建。它们不是在__init()__初始化时创建。然而描述符的值可以在初始化期间设置,描述符通常是作为类的一部分,在任何方法函数之外来构建的。 当所有者类被定义时,每个描述符对象都是被绑定到一个不同的类级别属性的描述符类实例。 被确认为一个描述符,一个类必须实现以下三个方法的任意组合。
有时,一个描述符类还将需要一个__init__()方法函数来初始化描述符的内部状态。 有两种基于已定义方法的描述符,如下所示: 1.非数据描述符:这种描述符定义__set__()或__delete__()或两者皆有。它不能定义__get__()。非数据描述符对象往往会被用作表达式的一部分。它可能是一个可调用对象,或者它可能有自己的属性或方法。一个不可变的非数据描述符必须实现__set__(),但可能只是抛出AttributeError。这些描述符设计时很简单,因为接口更灵活。
当我们考虑一个描述符的目的,我们还必须为数据作为描述符可以正常工作来考察三种常见用例,如下所示:
我们将仔细看下第一种情况。我们看看创建带有__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,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__()方法做适当的转换。 下面是一个单位描述符的超类,它在其他单位和标准单位之间做转换: class Unit: conversion = 1.0 def __get__(self,owner): return instance.kph * self.conversion def __set__(self,value): instance.kph = value / self.conversion 该类用简单的乘法和除法将标准单位转换为其他非标准单位,反之亦然。 通过这个超类,我们可以从一个标准单位定义一些转换。在前面的示例,标准单位是千米时(公里/小时)。 以下是这两个转换描述符 class Knots(Unit): conversion = 0.5399568 class MPH(Unit): conversion = 0.62137119 继承方法非常有用。唯一改变的是转换因子。这些类可用于处理涉及单位转换的值。我们可以处理英里每小时或可交换的节点。下面是一个标准单位的单位描述符,公里每小时: class KPH(Unit): def __get__(self,owner): return instance._kph def __set__(self,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属性。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |