2.39. 使用浮点数

浮点数看起来就像普通的小数,但它们的某些行为会让从未遇到过的读者大吃一惊 —— 而且其中一种行为在 MicroPython 上比在桌面 Python 上更为明显。本页讲解你应该预期什么,以及如何编写不会悄无声息地出错的浮点代码。

2.39.1. 精度

Python 的 float 是一个 IEEE 754 二进制浮点数。在大多数 MicroPython 构建上它是 单精度(32 位),而桌面 CPython 使用 双精度(64 位)。单精度大约携带七位十进制有效数字;双精度大约携带十五位。

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

可表示的范围在两端也更窄:量级大于约 3.4e38 的数会溢出为 inf,而小于约 1.2e-38 的数会舍入为零。

2.39.2. 比较浮点数

最常见的坑是用 == 测试相等:

>>> 0.1 + 0.2 == 0.3
False

两个表达式看起来都应该相等,但 0.1 + 0.2 的结果是最接近的可表示值,它并不正好等于 0.3。请改用 容差检查 —— 询问两个浮点数是否足够接近,而非是否完全相同:

if abs(a - b) < 1e-6:
    # close enough
    ...

容差的选择取决于值的尺度。当数字在 1 的量级左右时,固定的 1e-6 效果很好;而当值在多个数量级上变化时,相对 容差更合适。

math.isclose() 可以一次性处理这两种情况:

from math import isclose

isclose(0.1 + 0.2, 0.3)         # True
isclose(1.0e6 + 1, 1.0e6)       # True (within default tolerance)

两个关键字参数控制哪种“接近”算数:

  • rel_tol —— 相对 容差,默认 1e-9。如果两个值的差在较大者的这个比例之内,它们就匹配。适合任意尺度的通用比较。

  • abs_tol —— 绝对 容差,默认 0。如果两个值的差在这个固定数量之内,它们就匹配。

math.isclose() 只要满足任一容差就返回 True。对于大多数非零数对,默认值都没问题;陷阱在于其中一个值可能正好为零。相对容差检查相当于“差值 ≤ rel_tol × 最大值”,而最大值为零,所以检查总是失败:

>>> isclose(0.0, 1e-12)
False

绝对容差检查没有这种问题 —— 只要零是你可能拿来比较的一个值,就传入一个 abs_tol

>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True

2.39.3. 累加漂移

浮点数的长串求和在 MicroPython 上比在 CPython 上更快地损失精度,因为每一个中间结果都会被舍回 32 位精度:

total = 0.0
for _ in range(1000000):
    total += 0.1

print(total)        # noticeably off from 100000.0

对于精度很重要的重复加法,有两种模式可以帮上忙:

  • 累加到整数中 —— 只要这些值能缩放为整数,就以毫秒代替秒、以毫伏代替伏来运算,最后再一次性转换回来。

  • 分成较小的批次计算 再对各批次结果求和,这样每一次加法都发生在量级相近的值之间。

整数这边没有这种限制 —— MicroPython 的整数是任意精度的,跟 CPython 的一样。在你有选择余地的地方,对任何精度损失会累积的运算都优先使用整数算术。