起因
项目中用到了浮点数累加,来计算某种case出现的得分。并筛选出得分大于1的case。即:{python}sum(case_x_score_list) >= 1
其中有一组得分情况如下:case A [0.7, 0.2, 0.1]
按理应该满足 {python}0.7 + 0.2 + 0.1 == 1 >= 1
的判断条件。然而事情却没这么简单。
>>> 0.7 + 0.2 + 0.1
0.9999999999999999
>>> 0.7 + 0.2 + 0.1 == 1
False
看到结果我大为震撼。于是查阅了python浮点数计算的相关资料,特此记录。
浮点数原理
浮点数诸如0.1
, 0.125
在硬件中是以二进制方式来存储的。所以和我们日常使用的十进制有所区别。而且不幸的是,大部分的十进制小数都不能精确的表示为二进制。这导致在多数情况下,你输入的十进制浮点数都只能近似地以二进制浮点数形式储存在计算机中。
用十进制来理解这个问题显得更加容易一些。以 1/3
为例 。我们可以得到它在十进制下的一个近似值
0.3
或者,更近似的:
0.33
或者,更近似的:
0.333
以此类推。结果是无论写下多少的数字,它都永远不会等于 1/3 ,只是更加更加地接近 1/3 。
同样的道理,无论你使用多少位以 2 为基数的数码,十进制的 0.1 都无法精确地表示为一个以 2 为基数的小数。 在以 2 为基数的情况下, 1/10 是一个无限循环小数
0.0001100110011001100110011001100110011001100110011...
在任何一个位置停下,你都只能得到一个近似值。因此,在今天的大部分架构上,浮点数都只能近似地使用二进制小数表示,对应分数的分子使用每 8 字节的前 53 位表示,分母则表示为 2 的幂次
。在 1/10
这个例子中,相应的二进制分数是 3602879701896397 / 2 ** 55
,它很接近 1/10
,但并不是 1/10
。
如果我们想在终端把0.1
完整的打印出来,它会是这样:
>>> format(0.1, '.55f')
'0.1000000000000000055511151231257827021181583404541015625'
这就解释了上面{python}0.7 + 0.2 + 0.1 == 1
为什么不成立。因为这些小数本身就是近似值,相加得到的结果也是一个近似值。
这种情况是二进制浮点数的本质特性:它不是 Python 的错误,也不是你代码中的错误。 你会在所有支持你的硬件中的浮点运算的语言中发现同样的情况.
解决办法
既然浮点数计算没法绕过硬件限制,每次计算都会产生新的舍入错误,那到底还能不能用?
用还是能用,因为浮点数近似值对大部分场景来说以足够使用(每次浮点运算得到的 2**53 数码位都会被作为 1 个整体来处理)。例如可以使用decimal
来计算特定精度浮点数:
>>> from decimal import localcontext
>>> with localcontext() as ctx:
... ctx.prec = 3
... Decimal(0.7) + Decimal(0.2) + Decimal(0.1) == 1
...
True
其次对上面{python}0.7 + 0.2 + 0.1 == 1
的等式来说,可以利用python math提供的 {js}fsum()
方法来处理:
>>> math.fsum([0.7, 0.2, 0.1]) == 1
True
fsum有助于减少求和过程中的精度损失。 它会在数值被添加到总计值的时候跟踪“丢失的位”。 这可以很好地保持总计值的精确度, 使得错误不会积累到能影响结果总数的程度
参考文档
https://docs.python.org/zh-cn/3/tutorial/floatingpoint.html https://python3-cookbook.readthedocs.io/zh_CN/latest/c03/p02_accurate_decimal_calculations.html