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

    我们使用ratedistance参数创建了RTD1的实例,它们用于完成ratedistance修饰符中_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属性。