为什么 0.1 + 0.2 在你的代码中不等于 0.3
Source: Dev.to
print(0.1 + 0.2)
你本来期待看到 0.3。结果却得到 0.30000000000000004。
你的计算器显示 0.3。Excel 显示 0.3。你的大脑认为是 0.3。但你的代码却不这么认为。这并不是 Python 的 bug——每种编程语言都会出现这种情况。JavaScript、Java、C++、Ruby、Go——它们都以完全相同的方式背叛基本算术。
这个看似微小的怪癖已经导致了数百万美元的灾难。1991 年,爱国者导弹防御系统因累计的浮点误差未能拦截伊拉克的斯库德导弹,导致 28 名士兵丧生。金融系统每天因货币计算中的四舍五入误差损失数千美元。科学模拟产生垃圾结果。医疗剂量软件计算错误。
然而大多数开发者从未了解 为什么 会出现这种情况,或 如何 解决它。他们只是随意添加四舍五入函数,直到看起来正确为止。今天我们就来结束这种做法。
Source: …
真正的问题
计算机并不像你想的那样直接存储十进制数字。当你在代码中写下 0.1 时,计算机会将其转换为二进制——由 0 和 1 组成。但关键在于:大多数十进制分数在二进制中无法精确表示。
想想三分之一这个分数。在十进制中你会写成 0.333…,一直写下去。无论写多少个 3,都永远捕捉不到它的精确值。二进制对十分之一也有同样的问题,这就是为什么 0.1 会变成
0.0001100110011001100110011… (repeating forever)
你的计算机内存是有限的,所以它会截断这段无限的序列。截断后的版本接近 0.1,但并不完全相同。当你把两个近似数相加时,它们微小的误差会叠加并变得可见。
这会影响所有涉及小数的计算。乘法、除法、减法——它们都继承了这种根本的近似性。误差会随着每一次操作而累积。若在循环中进行一百万次计算,你的结果可能会偏离真实值很多。
为什么没人提醒你
- 学习盲点 – 初学者先学习整数和字符串,所以当他们接触浮点运算时,默认它像普通数学一样工作。
- 覆盖稀疏 – 计算机科学课程只会略提 IEEE‑754 浮点标准,却很少解释其在实际中的后果。许多毕业生从未了解为何金融软件把货币当作整数处理。
- 隐藏症状 – 大多数语言会自动对浮点数的显示进行四舍五入,因此
0.30000000000000004往往在日志中显示为0.3。只有在相等检查莫名失败或精度至关重要时,真相才会显现。 - 小规模不可见 – 加几条数字时看起来没问题;错误在处理海量数据、金融计算或迭代算法时才会暴露并放大。
Source: …
实际有效的修复
1. 停止使用精确相等比较浮点数
当 x 来自浮点运算时,切勿写 if x == 0.3。相反,检查数值是否 足够接近:
def float_equal(a, b, tolerance=1e-9):
"""Return True if a and b differ by less than tolerance."""
return abs(a - b) < tolerance
result = 0.1 + 0.2
if float_equal(result, 0.3):
print("Close enough!")
容差(通常称为 epsilon)定义了可接受的误差范围。对大多数应用来说,十亿分之一 (1e-9) 已足够;可根据精度需求进行调整。
2. 使用整数处理金钱
切勿使用浮点数表示货币。将最小单位存入整数(分、厘等):
price_cents = 1999 # $19.99
tax_cents = int(price_cents * 0.07) # 7 % tax
total_cents = price_cents + tax_cents
print(f"Total: ${total_cents / 100:.2f}")
所有算术运算都是精确的;仅在显示时再转换为十进制表示。
3. 使用十进制库进行精确的十进制运算
当需要真正的十进制精度(会计、科学测量、法律文档等)时,使用十进制类型。Python 自带一个:
from decimal import Decimal
a = Decimal('0.1')
b = Decimal('0.2')
print(a + b) # Exactly 0.3
重要提示: 将数字作为 字符串 传入。Decimal(0.1) 会先创建一个浮点数 (0.10000000000000000555…) 并继承误差。
4. 必要时提高精度
Python 的 float 是 64 位双精度,提供约 15‑17 位十进制有效数字。如果这仍不足,decimal 模块允许设置任意精度:
from decimal import Decimal, getcontext
getcontext().prec = 50 # 50 decimal places
result = Decimal(1) / Decimal(3)
print(result) # 0.33333333333333333333333333333333333333333333333333
更高的精度会降低计算速度,但有时正确性比速度更重要。
当精度攻击
金融系统是显而易见的受害者,但危险的范围更广:
- 科学模拟 – 数以百万计的迭代可能完全偏离轨道。
- 机器学习流水线 – 累积的舍入误差会降低模型精度。
- 游戏物理引擎 – 忽视浮点精度会导致物体穿墙等故障。
- GPS、天气预报、加密货币交易所、医疗设备 – 微小的传感器读数误差在计算传播后会变成灾难性后果。
一个著名的警示案例是 温哥华证券交易所指数。该指数于 1982 年以 1000.00 点启动,22 个月后莫名其妙跌至 524.811——远低于预期的约 1100 点——这是由于累积的浮点舍入误差导致的。
要点
浮点表示是一种 近似,而非 bug。了解其局限并使用合适的工具——容差比较、货币整数运算、十进制库或更高精度类型——可以保持程序的准确性和可靠性。
Source: …
生存策略
1. 知道何时需要精确
- 博客文章浏览量?使用浮点数即可。
- 银行账户余额?绝对 不能 使用浮点数。
- 如果你不能容忍任何误差——金融交易、法律文件、生命关键系统——请从一开始就使用精确算术。
2. 测试你的边界情况
- 编写单元测试,专门检查带循环小数的计算。
- 给函数喂入类似
0.1、0.01、0.001的值。 - 许多 bug 会在有人输入 $10.10 而不是整数时显现。
3. 了解你的工具
- 阅读你所使用的语言提供的 decimal 或 bignum 库的文档。
- 学习它们的怪癖:有些库有精度限制,另一些在不同操作下表现不同。
- 在决定方案之前 先 了解这些细节。
4. 注意误差累积
- 在循环中对成千上万的数求和时,小误差会叠加。
- 考虑使用专为精确求和设计的算法,例如 Kahan 求和,它会跟踪并补偿丢失的精度。
5. 记录你的决策
- 当你为金钱选择整数或为精度选择 decimal 时,留下一条注释解释 原因。
- 将来的你——或其他开发者——在想要通过改用浮点数“简化”时,会感激这段理由。
要点
计算机在小数上撒谎。它们一直如此,也将永远如此。这不是 bug——而是二进制算术在硬件层面的工作方式。
一旦你接受浮点数是近似值而非精确值,你就会写出更具弹性的代码。
- 下次看到奇怪的计算结果时,在假设代码出错之前,先检查是否是浮点精度导致的。
- 然后使用合适的工具:ε(epsilon)比较、整数运算或十进制库。
你的金融软件将保持平衡。你的科学模拟将收敛。你再也不会怀疑为什么 0.1 + 0.2 会违背你在小学学到的一切了。