Python 的 0.1 + 0.2 为什么不等于 0.3?拆开 IEEE 754 与那串死循环小数

0 阅读2分钟

几乎每个 Python 新手都会撞上这一幕:

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

这不是 Python 的 bug,也不是你代码写错了。这是二进制的宿命。

一、0.1 在内存里根本不是 0.1

浮点数按 IEEE 754 双精度(64 位)存:1 位符号 + 11 位指数 + 52 位尾数。问题出在"尾数"上——它是二进制小数。

十进制的 0.1,换成二进制是:

0.000110011001100110011001100110011...  (0011 无限循环)

跟十进制里 1/3 = 0.333... 一样,是个无限循环小数。而 double 的尾数只有 52 位,存不下,只能截断。

验证一下(CPython 3.13):

>>> float.hex(0.1)
'0x1.999999999999ap-4'
>>> float.hex(0.2)
'0x1.999999999999ap-3'
>>> struct.pack('>d', 0.1).hex()
'3fb999999999999a'

999999999999a 末尾那个 a,就是被四舍五入进去的零头。真正存进去的值是:

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

比 0.1 大约多 5.55e-18。所以 0.1 + 0.2 算出来是 0.30000000000000004——两个"零头"加起来溢出了最小精度位。

二、误差会累积,而且藏得很深

单次运算的误差小到看不见,攒起来就藏不住了:

>>> sum([0.1] * 10) == 1.0
True          # 巧了,这回正好对齐
>>> x = 0.0
>>> for _ in range(1_000_000): x += 0.1
>>> x
100000.00000133288      # 数学上应该是 100000.0

一百万次累加,误差累积到约 1.33e-6(实测耗时 130ms)。中间每一步都在"合法地"丢精度,没有任何报错。

更极端的两种情况:

>>> 1e16 + 1 == 1e16
True                    # 1 被彻底吃掉了
>>> 1e308 * 10
inf                     # 溢出成无穷,不报异常

1e16 + 1 == 1e16 特别值得记:double 在 1e16 这个量级上,相邻两个可表示数的间距已经是 2,中间的 1 根本不存在。

三、round 也不是你以为的那个 round

>>> round(2.675, 2)
2.67                    # 不是 2.68
>>> round(1.005, 2)
1.0                     # 不是 1.01

原因同样是底层表示:2.675 实际存的比 2.675 略小,所以被"舍"掉了。Python 用的还是银行家舍入(四舍六入五取偶),不是我们小学学的四舍五入。

四、怎么办:三种正确的姿势

1. 比较用 math.isclose,别用 ==

>>> math.isclose(0.1 + 0.2, 0.3)
True

2. 要精确就上 Decimal(注意:传字符串,别传 float)

>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')          # 干净
>>> Decimal(0.1) + Decimal(0.2)
Decimal('0.3000000000000000166533453694')   # 传 float 就白搭

金额计算必须用 Decimal('19.99') 这种字符串构造,从数据库读出来的也先转字符串。

3. 要完全精确用 Fraction

>>> Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10)
True

Fraction 走整数分子分母,零误差,代价是慢。

五、一句话记住

0.1 + 0.2 != 0.3 不是 Python 的错,是"用有限的二进制位去装无限的十进制小数"这件事本身就做不到。

记住三条就够:比较用 isclose、算钱用 Decimal、别信 round 能救你。

(实测环境:CPython 3.13,Windows;文中数字均可直接复现)