浮点数 · 0.1 + 0.2 为什么不等于 0.3

计算机用浮点数近似实数——本质是二进制的科学计数法。这一章讲清 IEEE 754 的结构、为什么 0.1 根本存不准、由此引出的第一条铁律:浮点数永远别用 == 比较,要用容差。

作者 David更新于 第 1 篇(共 21 篇)

浮点数 · 0.1 + 0.2 为什么不等于 0.3

序里那行 0.1 + 0.2 == False 是整个专栏的引子。要理解它,得先看清:计算机到底怎么存一个实数。

浮点数:二进制的科学计数法

实数有无穷多个,而计算机内存有限——不可能精确存下每一个。于是有了浮点数(floating-point):一种用固定位数近似表示实数的方案,本质是二进制的科学计数法。

十进制科学计数法把数写成 ±d.ddd×10e\pm d.ddd \times 10^{e};浮点数把它搬到二进制:

x=(−1)s×1.f×2ex = (-1)^{s} \times 1.f \times 2^{e}
  • ss:符号位(正负)。
  • ff:尾数(mantissa),决定精度(有几位有效数字)。
  • ee:指数,决定范围(能表示多大多小)。

最常用的 float64(双精度) 用 64 位:1 位符号 + 11 位指数 + 52 位尾数。float32(单精度)则是 1 + 8 + 23。位数是固定的,这就是一切误差的根源——有限的位,装不下无限的实数,只能四舍五入到最接近的那个能表示的值。

0.1 存不准

问题来了:很多十进制里「整齐」的小数,在二进制里是无限循环的。 就像 13=0.333…\frac{1}{3}=0.333\dots 在十进制里写不完,0.10.1 在二进制里也是无限循环:0.000110011001100…0.000110011001100\dots。而尾数只有 52 位,只能截断、四舍五入。所以计算机里存的 0.1,其实不是 0.10.1,而是最接近它的那个浮点数。用 Decimal 把真实存进去的值揪出来看:

from decimal import Decimal
print(Decimal(0.1))
# 0.1000000000000000055511151231257827021181583404541015625

看到了吗?0.1 存进去,实际是 0.10000...0555...——多出一点点尾巴。0.2、0.3 也各有各的尾巴。于是:

>>> 0.1 + 0.2
0.30000000000000004      # 两个近似值相加,尾巴凑到了小数点后 17 位
>>> 0.3
0.29999999999999998889...  # 而 0.3 自己的近似,又是另一个值

0.1 的近似 + 0.2 的近似,不等于 0.3 的近似——差了大约 10−1710^{-17}。== 于是返回 False。这不是谁的 bug,是有限精度的必然。

第一条铁律:浮点数别用 == 比

浮点数几乎永远不要用 == 判断相等——要用「容差」。 因为任何一串浮点运算都会累积微小的舍入误差,两个「数学上相等」的量,算出来往往差那么一丁点。正确姿势是判断它们足够接近:

# ✗ 危险
if a == b: ...

# ✓ 正确:差的绝对值小于一个小容差
if abs(a - b) < 1e-9: ...

# ✓ 更稳:NumPy 提供了带相对+绝对容差的现成函数
import numpy as np
np.isclose(0.1 + 0.2, 0.3)      # True

容差取多大?看你的量级和精度需求(下一章的机器精度给你标尺)。但「用容差、不用 ==」这条,是每个碰浮点数的程序员都该刻进肌肉的第一反应。

顺带认识:inf 和 nan

浮点数还有几个「特殊值」,迟早会遇到:

1.0 / 0.0      # 报错 ZeroDivisionError(Python 纯 float)
np.float64(1) / 0    # inf(NumPy:正无穷,还会 warning)
np.float64(0) / 0    # nan(Not a Number,0/0 这种没定义的结果)
np.inf - np.inf      # nan
  • inf(无穷):溢出或除以 0。
  • nan(非数):0/00/0、∞−∞\infty-\infty 这类没意义的运算。

nan 有个坑:它和任何值比较都返回 False,包括它自己! np.nan == np.nan 是 False。所以判断一个值是不是 nan,不能用 == np.nan,要用 np.isnan(x)。nan 还会「传染」——任何和 nan 的运算结果都是 nan,所以一个中途冒出的 nan 会悄悄污染整个计算,最后你只看到满屏 nan 却不知源头在哪。这是数值程序常见的调试噩梦。

小结与下一章

计算机用有限位数的浮点数近似实数(二进制科学计数法:符号 + 尾数 + 指数)。位数有限,导致像 0.1 这种十进制整数在二进制里存不准,0.1+0.2≠0.3 由此而来。第一条铁律:浮点数用容差比较,别用 ==;顺带认识会传染的 nan。

「存不准」到底能有多不准?这个「一丁点」误差的大小,是有精确标尺的——它叫机器精度。下一章我们量化它,并看看这些微小误差在长计算里怎么累积、又怎么用巧法子压住。👉 机器精度:误差有多大,以及怎么压住它。

参考文献

  • [1] David Goldberg.「What Every Computer Scientist Should Know About Floating-Point Arithmetic.」1991. —— 浮点数的权威长文。
  • [2] IEEE 754-2019.「Standard for Floating-Point Arithmetic.」—— 浮点数标准本身。
  • [3] Python 官方文档.「Floating Point Arithmetic: Issues and Limitations.」https://docs.python.org/3/tutorial/floatingpoint.html —— 通俗入门。

本页目录