0.1+0.2!=0.3, why?



通常情况下,在进行浮点运算时,会出现一些“匪夷所思”的结果。


我们都知道,1/3 等于 0.333…(无限循环),0.1 + 0.2 等于 0.3,但 Python 似乎不这么认为:

>>> 1/3
0.3333333333333333
>>>
>>> 0.1 + 0.2 == 0.3
False
>>>
>>> 0.1 + 0.2
0.30000000000000004

What?难道是眼花了?还有这种操作?(一脸懵逼。。。)

1

为何出现这种情况?

在看到上面出现的“错误”时,你可能会非常震惊,但它只不过是有关“浮点表示错误”的一个典型示例,类似的情况还有很多:

>>> 0.2 + 0.4        # 加法
0.6000000000000001
>>>
>>> 0.3 - 0.2
0.09999999999999998  # 减法
>>>
>>> 9.7 * 100
969.9999999999999    # 乘法
>>>
>>> 0.3 / 0.1
2.9999999999999996   # 除法

之所以会这样,与浮点数在内存中的存储方式有关:

在大多数现代计算机中,浮点数会被存储为精度为 53 位的二进制小数,只有具有有限的二进制小数表示法(可以用 53 位表示)的数字才被存储为一个精确的值,但并不是每个数字都有一个有限的二进制小数表示法。

例如,小数 0.1 有一个有限的十进制表示,但二进制表示却是无限的。正如分数 1/3 只能表示为无限循环小数 0.333…,分数 1/10 只能用二进制表示为无限循环小数 0.0001100110011… 一样。

具体请参考官方文档:Floating Point Arithmetic: Issues and Limitations(https://docs.python.org/3/tutorial/floatingpoint.html)。

出于这个原因,大部分小数都不能准确地存储在计算机中。因此,这是计算机硬件的局限性,而不是 Python 中的 bug。

2

精确处理

对于浮点运算过程中产生的一些误差,在有些需要精确计算的场合(例如:财务结算)是不可接受的。

好在 decimal 模块解决了这个烦恼 ,虽然浮点数的默认精度可达 17 位,但 decimal 模块可自定义精度:

>>> import decimal
>>>
>>> 0.1
0.1
>>>
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>>
>>> decimal.getcontext()           # 当前上下文
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[FloatOperation], traps=[InvalidOperation, DivisionByZero, Overflow])
>>>
>>> decimal.getcontext().prec      # 精度默认 28 位
28
>>>
>>> d = decimal.Decimal(1) / decimal.Decimal(9)
>>> d
Decimal('0.1111111111111111111111111111')
>>>
>>> decimal.getcontext().prec = 3  # 将精度修改为 3 位
>>>
>>> d = decimal.Decimal(1) / decimal.Decimal(9)
>>> d
Decimal('0.111')

除此之外,还可以用它进行精确计算:

>>> 1.2 * 2.50
3.0
>>>
>>> from decimal import Decimal as D
>>>
>>> D('0.1') + D('0.2')  # 这时的结果变为了 0.3
Decimal('0.3')
>>>
>>> D('1.2') * D('2.50')  
Decimal('3.00')          # 注意计算结果末尾的 0

注意:2.50 比 2.5 更精确,因为它有两个有效的小数位。

有人可能会问:既然 Decimal 这么好,为什么不每次都使用 Decimal 来代替 float 呢?其实主要是效率的原因,因为 float 运算要比 Decimal 运算更快。

那么,应该在何时使用 Decimal,而不是 float 呢?主要有以下情况:

  • 当在做金融应用时,需要精确表示;

  • 当想要控制所需的精度级别时;

  • 当想要实现有效的小数位概念时;

  • 当想要像在学校里学到的那样进行小数运算时。

3

浮点数比较

在处理浮点数计算时需要非常小心,尤其是进行浮点数的大小比较。

早期版本

如果是早期的  Python 版本,可以用下述方式进行比较:

def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
    return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)

可参考文档 PEP 485(https://www.python.org/dev/peps/pep-0485/#proposed-implementation)。

3.5 及之后的版本

在 Python 3.5 中,引入了 math.isclose() 和 cmath.isclose() 方法,用于判断两个值是否大致相等或相互“接近”,比较结果取决于 rel_tol 和 abs_tol。

  • rel_tol:相对容差 - (相对于 a 或 b 的较大绝对值)允许的误差量。

  • abs_tol:是一个最小的绝对容差范围 -- 对于接近于零的比较非常有用。

例如,指定 rel_tol 来比较两个值:

>>> import math
>>> a = 5.0
>>> b = 4.99998
>>> math.isclose(a, b, rel_tol=1e-5)
True
>>> math.isclose(a, b, rel_tol=1e-6)
False

同样地,也可以指定 abs_tol 进行比较:

>>> a = 5.0
>>> b = 4.99998
>>> math.isclose(a, b, abs_tol=0.00003)
True
>>> math.isclose(a, b, abs_tol=0.00001)
False

·END·
 

高效程序员

谈天 · 说地 · 侃代码 · 开车

长按识别二维码,解锁更多精彩内容

©️2020 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值