目录
- 问题
- 解决方案
- 讨论
问题
如果我们需要对小数执行精确的计算,并且不希望因为浮点数的误差带来影响,我们该怎么做?
解决方案
关于浮点数,一个人尽皆知的问题就是其无法精确地表达出所有十进制小数位,因此甚至简单的数字也会引入微小的误差。
a = 4.2
b = 2.1
print(a + b)
print((a + b) == 6.3)>>> 6.300000000000001
>>> False
而浮点数出现误差的原因,是因为在 Python 中,浮点数遵循 IEEE 754 标准,这点导致某些十进制的小数在转换为二进制数时,无法精确表示,如
# 十进制中,0.1 表示为
x = 0.1
# 但是在转换计算机的二进制时,0.1转换为
0.1 * 2 -> 整数部分0, 0.2 * 2 -> 整数部分0, 0.4 * 2 -> 整数部分0, 0.8 * 2 -> 整数部分1, 0.6 * 2 -> 整数部分1, 0.2 * 2 -> 整数部分0, ...
即b(x) = 0.000110001100011...
由于计算内存有限,不能存储无限循环小数,必须对小数进行四舍五入或者截断,至此,产生误差。
此时,如果我们需要更高的精度,则可以使用 decimal 模块
from decimal import Decimala = Decimal('4.2')
b = Decimal('2.1')
print(a + b)
print(a + b == Decimal('6.3'))
之所以 Decimal 对象可以精确计算,根本原因是因为其使用了不同于 IEEE 754 的表示形式来存储数值。Decimal 对象基于十进制数,而非二进制数,不直接在硬件上运行,而是以软件模拟十进制的方式进行计算。优点在于的确能够提供精确的十进制表示和计算,但是缺点很明显,区别于直接在硬件上计算,其会运行的较慢。
此外,decimal 模块还可控制计算过程中的各个方面,比如数字的位数以及四舍五入等等。其需要创建一个本地的上下文环境然后修改其设定。
from decimal import localcontext, Decimala = Decimal('1.3')
b = Decimal('1.7')
print(a / b)
with localcontext() as ctx:ctx.prec = 3print(a / b)
讨论
其实在真实世界中,极少有什么东西需要计算到小数点后17位,因此,在计算中引入微小误差不足挂齿。而且明显原生依托于硬件计算的浮点数运算性能要快上很多,如果要执行大量的计算,那性能问题就非常重要了。
这也就是说,误差就像病菌一样,无法完全忽略误差。很多时候,我们需要按照项目需求以及任务类型来做选择。
# 尤其要注意大数和小数加在一起的时候
nums = [1.23e+18, 1, -1.23e+18]
print(sum(nums))
>>> 0
# 通过使用 math.fsum() 方法解决
import math
print(math.fsum(nums))
>>> 1
综上所述,decimal 并非浮点数运算的最佳方案,或者说,没有最佳方案,选择只有最合适的吧。