引言,
默认float 类型储存双精度(double) 浮点数,可表达16到17个小数点.
从实现方式看,浮点数以二进制储存十进制的近似值.这可能导致执行结果和编码的预期效果不符合,造成一定量的缺陷,所以对精度有严格要求的场合,应该选择固定精度类型.
1.关于精度问题
一般可以通过float.hex 方法输入实际储存值的十六进制格式字符串,以检查执行结果为什么不同.
还可以使用该方式实现浮点值的精确传递,避免精度丢失
- bin() 转二进制
- int() 转10进制
- oct() 转8进制
- hex() 转16进制
In [1]: 0.1 * 3
Out[1]: 0.30000000000000004
In [2]: (0.1 * 3).hex() # 显然两个储存的不同
Out[2]: '0x1.3333333333334p-2'
In [3]: (0.3).hex() # 转换为16进制
Out[3]: '0x1.3333333333333p-2'
In [4]: s = (1/3).hex()
In [5]: s
Out[5]: '0x1.5555555555555p-2'
In [6]: float.fromhex(s) # 返回浮点数
Out[6]: 0.3333333333333333
对于简单的比较操作,可以尝试将浮点数限制在有效的固定精度内,但是考虑到round算法实现问题,更准确的做法是使用decimal.Decimal类型
In [13]: round(0.1 * 3, 2) == round(0.3, 2) # 避免不确定行,左右都是使用了固定精度
Out[13]: True
In [14]: round(0.1, 2) * 3 == round(0.3, 2) # 将round 返回值作为操作数,导致精度再度丢失
Out[14]: False
不同类型的数字之间,可以直接进行加减和比较运算的.
In [15]: 1.1 + 2
Out[15]: 3.1
In [16]: 1.1 < 2
Out[16]: True
In [17]: 1.1 == 1.100
Out[17]: True
2.转换
将整数或者字符串转换为浮点数很简单,且能自动处理字符串内的符号以及空白符问题,只超过有效精确度的时候,结果和字符串内容存在差异.
In [18]: float(100) # 正常
Out[18]: 100.0
In [19]: float("-100.123") #符号
Out[19]: -100.123
In [20]: float(" \t 100.213123 \n") # 空白符
Out[20]: 100.213123
In [21]: float("1.1234e2") # 科学计算法
Out[21]: 112.34
差异部分
In [22]: float("0.1234567890123456789")
Out[22]: 0.12345678901234568 # 显示不完全
返回来,将浮点数转换为整数的时候,有多重不同的方案可以供我们选择.可以直接减掉小数部分,或者分别往大小两个方向取整数.
In [23]: int (2.6)
Out[23]: 2
In [24]: from math import trunc,floor,ceil
In [25]: trunc(3.2),trunc(-3.2) # 截断小数部分
Out[25]: (3, -3)
In [26]: floor(3.2),floor(-3.2) # 向小数方向取最近整数
Out[26]: (3, -4)
In [27]: ceil(3.2),ceil(-3.2) # 往大数字方向取整数
Out[27]: (4, -3)
3.十进制浮点数
与float 基于硬件的二进制浮点数类型相比,decimal.Decimal 是十进制实现,最高可以提供28位有效精度,其准确表达十进制数和运算,不存在二进制近似值的问题.
In [1]: 1.1+2.2
Out[1]: 3.3000000000000003 # 结果只是与3.3相似
In [2]: (0.1 + 0.1 + 0.1 - 0.3) == 0 # 与预期结果不符
Out[2]: False
In [3]: from decimal import Decimal
# 使用Decimal之后
In [4]: Decimal("1.1") + Decimal("2.2")
Out[4]: Decimal('3.3')
In [5]: Decimal("0.1") + Decimal("0.1") + Decimal("0.1") - Decimal("0.3") == 0
Out[5]: True
在创建Decimal 实例时,应该传入一个准确数值,比如整数或者字符串等,如果是float类型的话,那么再构建之前,其中精度就已经丢失了.
In [6]: Decimal(0.1)
Out[6]: Decimal('0.1000000000000000055511151231257827021181583404541015625') # 精度已经丢失
In [7]: Decimal("0.1") # 只有在传入字符串或者整数的时候,精度才不会丢失.
Out[7]: Decimal('0.1')
需要的时候,可以通过上下文修改Decimal默认的28位精度
In [8]: from decimal import Decimal,getcontext
In [9]: getcontext()
Out[9]: Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[FloatOperation], traps=[InvalidOperation, DivisionByZero, Overflow])
In [10]: getcontext().prec = 2
In [11]: Decimal(1) / Decimal(7)
Out[11]: Decimal('0.14')
In [12]: Decimal(1) / Decimal(3)
Out[12]: Decimal('0.33')
更高阶一点的 用localcontext 限制一定的区域中的精度
In [14]: from decimal import localcontext
In [15]: with localcontext() as ctx:
...: ctx.prec = 2
...: print(getcontext().prec)
...: print(Decimal(1) / Decimal(7))
...:
2
0.14
除非有特别的需求,不然不要使用Decimal代替float,要知道其运算速度也会慢很多
4.奇舍偶入(并不是)
同样因为近似值和精度问题,造成float运行’四舍五入’ (round) 的时候操作存在不确定性,其结果会导致一些不易察觉的陷阱
In [18]: round(0.3)
Out[18]: 0
In [19]: round(0.5) # 这里应该是1才是正确的
Out[19]: 0
In [20]: round(1.5)
Out[20]: 2
按照round算法规则,按照林近数字距离远近来考虑是否进位,因此,四舍六入就是确定的,相关问题都一种在两边都是5的时候是否进位
按照以0.4为例子,其舍入后的相邻数字是0和1,从距离上看0自然是距离0.4更近一些
对于5 还要考虑后面是否还有小数位,如果有,那么左右距离就不可能是相等的,这自然需要进位
In [21]: round(0.5) # 与0,1距离相等,暂时不确定
Out[21]: 0
In [22]: round(0.5000001) # 哪怕是0.5后面的小数部分再小,那么他也是接近1的
Out[22]: 1
In [23]: round(1.25,1)
Out[23]: 1.2
In [24]: round(1.2500000001,1)
Out[24]: 1.3
剩下的,要看返回整数还是浮点数.如果是整数,就去邻近的偶数.
In [25]: round(0.5) # 0 --> 0.5 --> 1
Out[25]: 0
In [26]: round(1.5) # 1 --> 1.5 --> 2
Out[26]: 2
In [27]: round(2.5) # 2 --> 2.5 --> 3
Out[27]: 2
不同版本,规则存在差异,比如2.7中,round(2.5)返回值是3.0
从这点来看,我们应该谨慎对待此类行为差异,并且严格测试其造成的影响
如果依旧返回浮点数,事情就变得有点莫名其妙了,有些文章宣城 “奇舍偶入”,或者"五成双",也就是看5前一位小数的奇偶性来判断是否进位,但是事情并非如此
In [28]: round(1.25,1) # 偶舍
Out[28]: 1.2
In [29]: round(1.245,2) # 偶入
Out[29]: 1.25
In [30]: round(2.675,2) # 奇 舍
Out[30]: 2.67
In [31]: round(2.375,2) # 奇 入
Out[31]: 2.38
对此官方文档宣城这并不是错误,而是属于事出有因,对此我们可以改用Decimal,按照需求选取可控的进位方案.
In [35]: from decimal import Decimal,ROUND_HALF_UP
In [35]: def roundx(x,n):
...: return Decimal(x).quantize(Decimal(n), ROUND_HALF_UP) # 严格按照四舍五入进行
In [36]: roundx("1.24",".1")
Out[36]: Decimal('1.2')
In [37]: roundx("1.26",".1")
Out[37]: Decimal('1.3')
In [38]: roundx("1.245",".1")
Out[38]: Decimal('1.2')
In [39]: roundx("1.675",".1")
Out[39]: Decimal('1.7')
In [40]: roundx("1.375",".1")
Out[40]: Decimal('1.4')