7.3 创建一个数字类

    接下来会定义新的数值类型。由于Python已经提供了不定精度的整数类型、实分数、标准浮点数以及货币计算用到的小数,因而简化了这项任务。我们将定义一个“带比例”的数字类。这个类包含了一个整数和一个比例因数,可用于货币计算。对于世界上的许多货币来说,可以进行100以内的货币计算。

    使用比例计算的好处是可以通过使用底层的硬件指令来简化实现。为了利用硬件完成计算,可将这个模块用 C语言完成。而创建一种新的比例计算又显得有些多余,因为在decimal的包中已经对小数计算提供了很不错的支持。

    我们会命名它为FixedPoint类,因为小数的小数点位数是固定的。而比例因数定义为一个整数,通常为10的N次幂。原则上,使用2的N次幂作为比例因数会更快,但并不适合货币计算。

    使用2的N次幂作为比例因数会快的原因是可以将value (2*scale)替换为value << scale,将value/(2 scale)替换为value &gt;&gt; scale。而左右位运算通常由硬件指令完成,因此相比乘除运算会更快。

    理想情况下,比例因数为10的N次幂,但不会强制要求这一点。为了同时追踪次方数和比例因数,这里可作为一个扩展点。可将2存为次方数,而102 - 100则为因数。我们简化了类的实现,只追踪因数。

    7.3.1 FixedPoint的初始化

    我们将从初始化部分开始,包含了从不同类型到FixedPoint值的转换操作,如下所示。

    import numbers
    import math
    class FixedPoint( numbers.Rational ):
      slots = ( "value", "scale", "defaultformat" )
      def new( cls, value, scale=100 ):
        self = super().new(cls)
        if isinstance(value,FixedPoint):
          self.value= value.value
          self.scale= value.scale
        elif isinstance(value,int):
          self.value= value
          self.scale= scale
        elif isinstance(value,float):
          self.value= int(scale*value+.5) # Round half up
          self.scale= scale
        else:
          raise TypeError
        digits= int( math.log10( scale ) )
        self.defaultformat= "{{0:.{digits}f}}".format(digits=digits)
        return self
      def str( self ):
        return self.format( self.defaultformat )
      def repr( self ):
        return "{class.name:s}({value:d},scale={scale:d})".
    format( class=self._class
    , value=self.value, scale=self.scale )
      def __format
    ( self, specification ):
        if specification == "": specification= self.default_format
        return specification.format( self.value/self.scale ) # no
    rounding
      def numerator( self ):
        return self.value
      def denominator( self ):
        return self.scale

    FixedPoint类继承自numbers.Rational。接下来会包含两个整数值scalevalue,以及分数的一般定义。这将需要定义大量的特殊方法,因为初始化是针对不可变对象的,因此会重写new()方法而非init()方法。为了阻止添加属性,限制了slots的数量。初始化包括了如下的几种转换。

    • 如果赋值的对象类型为FixedPoint,将复制内部属性并创建新的FixedPoint对象,就是对原对象进行克隆。尽管它将有唯一的ID,但它将有相同的哈希值用来比较对象是否相等,使得克隆对象和原对象基本没有区别。
    • 如果赋值的对象类型为整数或有理数(int或float),它们用于为value和scale属性赋值。
    • 可以加入对decimal.Decimal和fractions.Fraction的处理逻辑以及字符串解析。

    我们定义了3种特殊方法来返回字符串:ser()repr()format()。对于格式化的操作,会重用格式规范语言中已有的浮点数功能。既然是有理数操作,因此还需提供分子和分母的相应操作方法。

    我们仍可以从封装已有的fractions.Fraction类为开始。而且,会看几种不同的四舍五入规则。在使用这个类解决具体问题之前,应对此进行合理谨慎的定义。

    7.3.2 定义固定小数点位数的二进制算术运算符

    创建一个新的数字类的唯一原因就是要重载算术运算符。每个FixedPoint对象包含了两部分:valuescale,可以看作是这种形式的: 空标题文档 - 图1

    在如下的例子中,使用了一种正确但并不高效的浮点数表达式来完成代数运算。接下来会讨论一种相对高效的实现方式,纯整数操作。

    加法(和减法)的一般形式是这样的: 空标题文档 - 图2 A+B=。可是它产生了多位无效的精度。

    比如使用9.95加上12.95,我们会得到(原则上)229000/10000。可被约分化简为2290/100,进一步化简为229/10,而它已经不再是美分了。在实际场景中,对于分数不会追求约分到最简形式,以确保它们仍能表达美分或米尔。

    对于 空标题文档 - 图3 ,可以想到两个例子。

    • 比例因数匹配:这种情况下,所求的和为 空标题文档 - 图4 。当进行FixedPoint或整数类型加法运算时,同样会有效。因为可以让整数在计算时也使用比例因数。
    • 比例因数不匹配:正确的做法是先进行通分,即= max(As, Bs)。从这点来看,可以计算 空标题文档 - 图5空标题文档 - 图6 。这些比例因数中的一个值将为1,另一个会小于1。先进行通分,得到代数式: 空标题文档 - 图7 。这个等式在两种情况下可化简,一种是因数为1,另一种则为10的指数的情况。

    无法对乘法做真正意义上的优化。对于基本的式子 空标题文档 - 图8 。当对FixedPoint的值做乘法运算时,精度会提高。

    除是乘法的逆运算, 空标题文档 - 图9 。如果A和B的比例相同,那么就可以再稍微优化一下,把这些值进行约分。然而,这会导致误差范围从美分变到全部,这种做法不是很妥当。下例是一些前置运算符的模板定义。

    def add( self, other ):
      if not isinstance(other,FixedPoint):
        newscale= self.scale
        newvalue= self.value + otherself.scale
      else:
        new_scale= max(self.scale, other.scale)
        new_value= (self.value
    (newscale//self.scale)
        + other.value(newscale//other.scale))
      return FixedPoint( int(newvalue), scale=new_scale )
    def __sub
    ( self, other ):
      if not isinstance(other,FixedPoint):
        new_scale= self.scale
        new_value= self.value - other
    self.scale
      else:
        newscale= max(self.scale, other.scale)
        newvalue= (self.value(new_scale//self.scale)
        - other.value
    (newscale//other.scale))
      return FixedPoint( int(newvalue), scale=newscale )
    def mul( self, other ):
      if not isinstance(other,FixedPoint):
        new_scale= self.scale
        new_value= self.value other
      else:
        new_scale= self.scale
    other.scale
        new_value= self.value * other.value
      return FixedPoint( int(new_value), scale=new_scale )
    def __truediv
    ( self, other ):
      if not isinstance(other,FixedPoint):
        new_value= int(self.value / other)
      else:
        new_value= int(self.value / (other.value/other.scale))
      return FixedPoint( new_value, scale=self.scale )
    def __floordiv
    ( self, other ):
      if not isinstance(other,FixedPoint):
        new_value= int(self.value // other)
      else:
        new_value= int(self.value // (other.value/other.scale))
      return FixedPoint( new_value, scale=self.scale )
    def __mod
    ( self, other ):
      if not isinstance(other,FixedPoint):
        new_value= (self.value/self.scale) % other
      else:
        new_value= self.value % (other.value/other.scale)
      return FixedPoint( new_value, scale=self.scale )
    def __pow
    ( self, other ):
      if not isinstance(other,FixedPoint):
        new_value= (self.value/self.scale) other
      else:
        new_value= (self.value/self.scale)
    (other.value/other.
    scale)
      return FixedPoint( int(new_value)*self.scale, scale=self.scale
    )

    对于简单的加减和乘法运算,为了消除一些相对缓慢的浮点产生的中间结果,我们可以使用被进一步优化的版本。

    对于这两种除法mod()pow()方法,并没有针对浮点数除法进行优化。相反,我们提供了一种Python中的实现方式以及单元测试,它们将作为优化和重构的出发点。

    除法操作可以适当地减小比例因数,这点是重要的。然而,有时也是不值得的。对于并发场景,也许会使用非并发的值(小时)除以汇率(美元)来得到像每小时的美元这样的结果。适当的结果会是整数,比例为1,不过或许也希望结果会以分为单位,比例为100。这种实现可确保运算符左边操作数所需的精度。

    7.3.3 定义FixedPoint一元算术运算符

    如下是一元运算符函数的定义。

    def abs( self ):
      return FixedPoint( abs(self.value), self.scale )
    def float( self ):
      return self.value/self.scale
    def int( self ):
      return int(self.value/self.scale)
    def trunc( self ):
      return FixedPoint( math.trunc(self.value/self.scale), self.
    scale )
    def ceil( self ):
      return FixedPoint( math.ceil(self.value/self.scale), self.
    scale )
    def floor( self ):
      return FixedPoint( math.floor(self.value/self.scale), self.
    scale )
    def round( self, ndigits ):
      return FixedPoint( round(self.value/self.scale, ndigits=0),
    self.scale )
    def neg( self ):
      return FixedPoint( -self.value, self.scale )
    def pos( self ):
      return self

    对于round()trunc()ceil()floor()运算符来说,可通过Python类库中的函数来实现。可以对它们进一步优化,但我们这里只取浮点数的近似值,并使用它来构造最终结果。这些方法确保了FixedPoint对象可以与很多算术运算函数进行有效的交互。在Python中有很多运算符,这还不是全部,这里还没有包含比较运算符和位运算符。

    7.3.4 实现FixedPoint反向运算符

    反向运算符会在如下两种场景中用到。

    • 右操作数类是左操作数类的子类。这种情况下,反向运算符会优先选择子类中的实现。
    • 左操作数的类没有实现所需的特殊方法。这种情况下,将使用右操作数的反向特殊方法。

    下表列出了反向特殊方法与运算符之间的映射关系。


    方法

    运算符

    object. radd (self, other)

    +

    object. rsub (self, other)

    -

    object. rmul (self, other)


    object. rtruediv (self, other)

    /

    object. rfloordiv (self, other)

    //

    object. rmod (self, other)

    %

    object. rdivmod (self, other)

    divmod()

    object. rpow (self, other[, modulo])

    Pow() ,*

    这些反向运算符特殊方法也可使用公共的模板来创建。由于它们是反向的,进行减、除、取模以及乘方运算时,顺序是很重要的。对于可交换的运算,例如加和乘运算,顺序就不是很重要。如下是一些反向运算符的定义。

      def radd( self, other ):
        if not isinstance(other,FixedPoint):
          newscale= self.scale
          newvalue= otherself.scale + self.value
        else:
          new_scale= max(self.scale, other.scale)
          new_value= (other.value
    (newscale//other.scale)
          + self.value(newscale//self.scale))
        return FixedPoint( int(newvalue), scale=new_scale )
      def __rsub
    ( self, other ):
        if not isinstance(other,FixedPoint):
          new_scale= self.scale
          new_value= other
    self.scale - self.value
        else:
          newscale= max(self.scale, other.scale)
          newvalue= (other.value(new_scale//other.scale)
          - self.value
    (newscale//self.scale))
        return FixedPoint( int(newvalue), scale=newscale )
      def rmul( self, other ):
        if not isinstance(other,FixedPoint):
          new_scale= self.scale
          new_value= otherself.value
        else:
          new_scale= self.scale
    other.scale
          new_value= other.value*self.value
        return FixedPoint( int(new_value), scale=new_scale )
      def __rtruediv
    ( self, other ):
        if not isinstance(other,FixedPoint):
          new_value= self.scale*int(other / (self.value/self.scale))
        else:
          new_value= int((other.value/other.scale) / self.value)
        return FixedPoint( new_value, scale=self.scale )
      def __rfloordiv
    ( self, other ):
        if not isinstance(other,FixedPoint):
          new_value= self.scale*int(other // (self.value/self.
    scale))
        else:
          new_value= int((other.value/other.scale) // self.value)
        return FixedPoint( new_value, scale=self.scale )
      def __rmod
    ( self, other ):
        if not isinstance(other,FixedPoint):
          new_value= other % (self.value/self.scale)
        else:
          new_value= (other.value/other.scale) % (self.value/self.
    scale)
        return FixedPoint( new_value, scale=self.scale )
      def __rpow
    ( self, other ):
        if not isinstance(other,FixedPoint):
          new_value= other (self.value/self.scale)
        else:
          new_value= (other.value/other.scale)
    self.value/self.
    scale
        return FixedPoint( int(new_value)*self.scale, scale=self.scale
    )

    我们已经对每种运算符对应的数学运算进行了尝试。思路就是以简单的方式交换每种操作数。这是最常见的场景。匹配的正向和反向的方法可以简化代码审查的工作。

    使用正向运算符,并不会对除法、取模和乘方运算符进行优化。当FixedPoint转为浮点数,再转换回来时会导致数值不精确。

    7.3.5 实现FixedPoint比较运算符

    以下是6组比较运算符以及它们对应的特殊方法。


    方法

    运算符

    object. lt (self, other)

    <

    object. le (self, other)

    <=

    object. eq (self, other)

    ==

    object. ne (self, other)

    !=

    object. gt (self, other)

    >

    object. ge (self, other)

    >=

    is运算符会比较对象的ID。想不到合适的目的来重写此行为,因为相对于其他的特殊类而言,它是独立的。in比较运算符由object.contains( self, value )实现。这对于数值来说是没有意义的。

    可以注意到关于比较的测试是一项特别的工作。由于浮点数是近似值,在进行浮点数的比较测试时就要非常小心。我们需要了解它们的值是否在一个足够小的范围内,即最小误差值,而不应写为a == b。一般要比较浮点数的近似值的表达式为abs(a - b) <= eps。更确切一些,可以写作 abs(a - b)/a <= eps

    FixedPoint类中,使用比例来定义两个浮点数值可被视为相等的程度。对于比例100来说,最小误差值为0.01。可实际上我们会更保守一些,当比例为100时,会使用0.005作为最小误差值。

    进一步说,需要判断FixedPoint(123, 100)FixedPoint(1230,1000)是否相等。尽管在数学上是等同的,可一个单位是美分,一个是米尔,这也算是两个数精度不同的一种原因。使用额外的有效位可来标识在比较时不视作相等,如果这样做,也当确保哈希值是不同的。

    辨别不同的比值对于应用程序来说是不合适的。因为我们期望FixedPoint(123, 100)FixedPoint(1230, 1000)两数是相等的。这同样也是hash()函数实现的背后所假设的。以下是FixedPoint类中比较操作的实现。

      def eq( self, other ):
        if isinstance(other, FixedPoint):
          if self.scale == other.scale:
            return self.value == other.value
          else:
            return self.value*other.scale//self.scale == other.
    value
        else:
          return abs(self.value/self.scale - float(other)) < .5/
    self.scale
      def ne( self, other ):
        return not (self == other)
      def le( self, other ):
        return self.value/self.scale <= float(other)
      def lt( self, other ):
        return self.value/self.scale < float(other)
      def ge( self, other ):
        return self.value/self.scale >= float(other)
      def gt( self, other ):
        return self.value/self.scale > float(other)

    每个比较函数需要接收一个非FixedPoint类型的值。唯一的要求是另一个值必须包含浮点数的表示方式。而我们已经为FixedPoint对象定义了一个float()方法,在比较两个FixedPoint值时,比较运算符就会完好地工作。

    不必为所有的6种比较操作提供实现。@fuctools.total_ordering装饰器可以从两个FixedPoint值中生成缺失的方法。我们会在第8章“装饰器和mixin——横切方面”中再次回顾这部分内容。