3.5 创建修饰符
修饰符可看作属性的访问中介。修饰符类可以被用来获取、赋值或删除属性值,修饰符对象通常在类定义时被创建。
修饰符模式有两部分:拥有者类(owner class)和属性修饰符(attribute descriptor)。拥有者类使用一个或多个修饰符作为它的属性。在修饰符类中可以定义获取、赋值和删除的函数。一个修饰符类的实例将作为拥有者类的属性。
特性是基于拥有者类的函数。修饰符不同于特性,与拥有者类之间没有耦合。因此,修饰符通常可以被重用,是一种通用的属性。拥有者类可同时包含同一个修饰符类的不同实例,管理相似行为的属性。
和属性不同,修饰符是在类级别定义的。它的引用并非在init()初始化函数中被创建。修饰符可在初始化过程中被赋值,修饰符通常作为类定义的一部分,处于任何函数之外。
当定义拥有者类时,每个修饰符对象都是修饰符类的实例,绑定在类级别的属性上。
为了标识为修饰符,修饰符类必须实现以下3个方法的一个或多个。
- 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()函数中初始化修饰符内部的一些状态。
基于方法的定义,如下是两种不同的修饰符类型。
- 非数据修饰符:这类修饰符需要定义set()或delete()或两者皆有,但不能定义get()。非数据修饰符对象经常用于构建一些复杂表达式的逻辑。它可能是一个可调用对象,可能包含自己的属性或方法。一个不可变的非数据修饰符必须实现set()函数,而逻辑只是单纯地抛出AttributeError异常。这类修饰符的设计相对简单一些,因为接口更灵活。
- 数据修饰符:这类修饰符至少要定义get()函数。通常,可通过定义get()和set()函数来创建一个可变对象。这类修饰符不能定义自己内部的属性或方法,因为它通常是不可见的。对修饰符属性的访问,也相应地转换为对修饰符中的get()、set()或delete__()方法的调用。这样对设计是一个挑战,因此不会作为首要选择。关于修饰符的使用有大量的例子。在Python中使用修饰符的场景主要有如下几点。
- 类内部的方法被实现为修饰符。它们是非数据修饰符,应用在对象和不同的参数值上。
- property()函数是通过为命名的属性创建数据修饰符来实现的。
- 类方法或静态方法被实现为修饰符,修饰符作用于类而非实例。
在第11章“用SQLite保存和获取对象”中,我们会讲到对象关系映射,如何大量使用修饰符的ORM类,完成从Python类定义到SQL表和列的映射。
当设计修饰符时,通过考虑以下3种常见的场景。
- 修饰符对象包含或获取数据。在这种情况下,修饰符对象的self变量是相关的并且修饰符是有状态的。使用数据修饰符时,get()方法用于返回内部数据。使用非数据修饰符时,由修饰符中其他方法或属性提供数据。
- 拥有者类实例包含数据。这种情况下,修饰符对象必须使用instance参数获取拥有者对象中的数据。使用数据修饰符时,get()函数从实例中获取数据。使用非数据修饰符时,由修饰符中其他方法提供数据。
- 拥有者类包含数据。在这种情况下,修饰符对象必须使用owner参数。由修饰符实现的静态方法或类方法的作用范围通常是全局的,这种做法是常见的。
我们会详细看一下第1种情况。使用get_()和set()函数创建数据修饰符,以及不使用get()方法的情况下创建非数据修饰符。
第2种情况(数据包含在拥有者实例中),正是@property装饰器的用途。比起传统的特性,修饰符带来的好处是,它把计算逻辑从拥有者类搬到了修饰符类中。而完全采用这样的设计思路来设计类是片面的,有些场景不能获得最大的收益。如果计算逻辑相当复杂,使用策略模式则更好。
对于第3种情况,@staticmethod和@classmethod装饰器的实现就是很好的例子。此处不再赘述。
3.5.1 使用非数据修饰符
经常会遇到一些对象,内部的属性值是紧密结合的。为了举例说明,在这里看一些和测量单位紧密相关的数值。
以下代码实现了一个简单的非数据修饰符类的实现,但未包含get()函数。
class UnitValue1:
"""Measure and Unit combined."""
def init( self, unit ):
self.value= None
self.unit= unit
self.defaultformat= "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 RTD1:
rate= UnitValue1( "kt" )
time= UnitValue1( "hr" )
distance= UnitValue1( "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参数创建了RTD1的实例,它们用于完成rate和distance修饰符中_set()函数的计算逻辑。
当调用str(m1)函数时,会调用RTD1中全局的str()函数,进而调用了速率、时间和距离修饰符的_format()函数,并会返回带有单位的数值。
由于非数据修饰符不包含get()函数,也没有返回内部数值,因此只能直接访问各个元素值来获得数据。
3.5.2 使用数据修饰符
数据修饰符使得设计变得更复杂了,因为它的接口是受限制的。它必须包含get()方法,并且只能包含set()方法和delete()方法。与接口相关的限制:可以包含以上方法的1~3个,不能包含其他方法。引入额外的方法将意味着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
以上的类通过简单的乘除运算实现了标准单位和非标准单位的互转。
使用这个基类,可以定义一些标准单位的转换。在之前的例子中,标准单位是KPH(千米每小时)。
以下是两个转换修饰符类。
class Knots( Unit ):
conversion= 0.5399568
class MPH( Unit ):
conversion= 0.62137119
从基类继承的方法完成了此过程的实现,唯一的改变是转换因数。这些类可用于包含单位转换的数值,可以用在MPH(英里每小时)或海里的转换。以下是一个标准单位的修饰符定义千米每小时。
class KPH( Unit ):
def get( self, instance, owner ):
return instance.kph
def _set( self, instance, value ):
instance._kph= value
这个类仅仅是定义了一个标准,因此没有任何转换逻辑。它使用了一个私有变量来保存KPH中的速度值。避免算术转换只是一种优化技巧,以防止任何对公有属性的引用,这是避免无限递归的前提。
以下是一个类,包含了给定测量的一些转换过程。
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)
对于不同的单位来说,每一个类级别的属性都是一个修饰符,在get和set函数中提供转换过程的实现,可以使用这个类来转换速度之间的各种单位。
以下是使用这个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类的对象,设置了不同的修饰符。例子中,我们设置了knots(海里)修饰符。
当数值需要显示在一个格式化的字符串中时,修饰符中的get()方法就会被调用。这些函数从拥有者类的对象中获取KPH(千米每小时)的属性值,设置转换因数并返回结果。
KPH(千米每小时)属性也使用了一个修饰符。这个修饰符没有做任何转换。然而,只是简单地返回了拥有者类对象中缓存的一个私有的数值。当使用KPH和Knots修饰符时,需要拥有者类实现一个KPH属性。
