7.3 创建一个数字类
接下来会定义新的数值类型。由于Python已经提供了不定精度的整数类型、实分数、标准浮点数以及货币计算用到的小数,因而简化了这项任务。我们将定义一个“带比例”的数字类。这个类包含了一个整数和一个比例因数,可用于货币计算。对于世界上的许多货币来说,可以进行100以内的货币计算。
使用比例计算的好处是可以通过使用底层的硬件指令来简化实现。为了利用硬件完成计算,可将这个模块用 C语言完成。而创建一种新的比例计算又显得有些多余,因为在decimal的包中已经对小数计算提供了很不错的支持。
我们会命名它为FixedPoint类,因为小数的小数点位数是固定的。而比例因数定义为一个整数,通常为10的N次幂。原则上,使用2的N次幂作为比例因数会更快,但并不适合货币计算。
使用2的N次幂作为比例因数会快的原因是可以将value (2*scale)替换为value << scale,将value/(2 scale)替换为value >> 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。接下来会包含两个整数值scale和value,以及分数的一般定义。这将需要定义大量的特殊方法,因为初始化是针对不可变对象的,因此会重写new()方法而非init()方法。为了阻止添加属性,限制了slots的数量。初始化包括了如下的几种转换。
- 如果赋值的对象类型为FixedPoint,将复制内部属性并创建新的FixedPoint对象,就是对原对象进行克隆。尽管它将有唯一的ID,但它将有相同的哈希值用来比较对象是否相等,使得克隆对象和原对象基本没有区别。
- 如果赋值的对象类型为整数或有理数(int或float),它们用于为value和scale属性赋值。
- 可以加入对decimal.Decimal和fractions.Fraction的处理逻辑以及字符串解析。
我们定义了3种特殊方法来返回字符串:ser()、repr()和format()。对于格式化的操作,会重用格式规范语言中已有的浮点数功能。既然是有理数操作,因此还需提供分子和分母的相应操作方法。
我们仍可以从封装已有的fractions.Fraction类为开始。而且,会看几种不同的四舍五入规则。在使用这个类解决具体问题之前,应对此进行合理谨慎的定义。
7.3.2 定义固定小数点位数的二进制算术运算符
创建一个新的数字类的唯一原因就是要重载算术运算符。每个FixedPoint对象包含了两部分:value和scale,可以看作是这种形式的:
。
在如下的例子中,使用了一种正确但并不高效的浮点数表达式来完成代数运算。接下来会讨论一种相对高效的实现方式,纯整数操作。
加法(和减法)的一般形式是这样的:
A+B=。可是它产生了多位无效的精度。
比如使用9.95加上12.95,我们会得到(原则上)229000/10000。可被约分化简为2290/100,进一步化简为229/10,而它已经不再是美分了。在实际场景中,对于分数不会追求约分到最简形式,以确保它们仍能表达美分或米尔。
对于
,可以想到两个例子。
- 比例因数匹配:这种情况下,所求的和为
。当进行FixedPoint或整数类型加法运算时,同样会有效。因为可以让整数在计算时也使用比例因数。 - 比例因数不匹配:正确的做法是先进行通分,即= max(As, Bs)。从这点来看,可以计算
和
。这些比例因数中的一个值将为1,另一个会小于1。先进行通分,得到代数式:
。这个等式在两种情况下可化简,一种是因数为1,另一种则为10的指数的情况。
无法对乘法做真正意义上的优化。对于基本的式子
。当对FixedPoint的值做乘法运算时,精度会提高。
除是乘法的逆运算,
。如果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 - otherself.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= otherself.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.scaleother.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——横切方面”中再次回顾这部分内容。
