15. 浮点算术争议限制

浮点数计算机硬件是以基数 2 (二进制) 小数表示例如十进制 小数 0.625 6/10 + 2/100 + 5/1000,同样 二进制 小数 0.101 1/2 + 0/4 + 1/8。 小数具有相同唯一区别在于第一成了基数 10 小数形式第二基数 2 小数形式

不幸大多数十进制小数不能精确表示二进制小数导致大多数情况输入十进制浮点数只能近似二进制浮点数形式储存计算机

十进制理解这个问题显得更加容易一些考虑分数 1/3 。我们可以得到十进制近似

0.3

或者似的,:

0.33

或者似的,:

0.333

以此类推结果无论写下多少数字永远不会等于 1/3 ,只是更加更加接近 1/3 。

同样道理无论使用多少 2 基数数码十进制 0.1 无法精确表示 2 基数小数 2 基数情况, 1/10 无限循环小数

0.0001100110011001100110011001100110011001100110011...

任何位置停下只能得到近似因此今天大部分架构浮点数只能近似使用二进制小数表示对应分数分子使用 8 字节 53 表示分母表示 2 1/10 这个例子相应二进制分数 3602879701896397 / 2 ** 55 ,接近 1/10 ,不是 1/10 。

由于显示方式大多数用户不会意识这个差异存在。 Python 打印计算机存储二进制十进制近似大部分计算机如果 Python 0.1 二进制对应准确十进制打印出来将会显示这样:
>>>

0.1
0.1000000000000000055511151231257827021181583404541015625

大多数认为有用数位因此 Python 通过改为显示保留管理数位:
>>>

1 / 10
0.1

牢记即使输出结果看起来好像就是 1/10 精确实际储存只是接近 1/10 计算机表示二进制分数

有趣许多不同十进制共享相同接近近似二进制小数例如, 0.1 、 0.10000000000000001 、 0.1000000000000000055511151231257827021181583404541015625 全都近似 3602879701896397 / 2 ** 55 。由于所有这些十进制具有相同近似因此可以显示其中任何同时仍然保留不变 eval(repr(x)) == x 。

历史上,Python 提示内置 repr() 函数选择具有 17 有效数字显示 0.10000000000000001。 Python 3.1 开始,Python(大多数系统现在能够选择这些表示简单显示 0.1 。

注意这种情况二进制浮点数本质特性不是 Python 错误不是代码中的错误所有支持硬件中的浮点运算语言发现同样情况虽然某些语言默认状态所有输出模块不会 显示 这种差异)。

想要美观输出可能希望使用字符串格式化产生限定长度有效:
>>>

format(math.pi, '.12g') #
12 有效数位
'3.14159265359'

format(math.pi, '.2f') #
小数点 2 数位
'3.14'

repr(math.pi)
'3.141592653589793'

必须重点了解实际上只是假象只是真正机器进行操作 显示 而已

假象可能导致另一假象例如由于这个 0.1 并非真正 1/10, 0.1 相加无法恰好得到 0.3:
>>>

0.1 + 0.1 + 0.1 == 0.3
False

而且由于这个 0.1 无法精确表示 1/10 这个 0.3 无法精确表示 3/10 使用 round() 函数进行预先没用:
>>>

round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
False

虽然这些数字无法精确表示其所代表实际但是可以使用 math.isclose() 函数进行精确比较:
>>>

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

或者可以使用 round() 函数大致比较近似程度
>>>

round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
True

二进制浮点运算许多这样令人惊讶情况有关 "0.1" 问题在下面 "表示错误" 精确详细描述参阅 Examples of Floating Point Problems 获取针对二进制浮点运算机制实践各种常见问题概要说明参阅 The Perils of Floating Point 获取其他常见意外现象完整介绍

正如文章结尾,“问题简单答案。” 但是不必过于担心浮点数问题! Python 浮点运算中的错误浮点运算硬件继承大多数机器每次浮点运算得到 2**53 数码都会作为 1 整体处理大多数任务足够确实需要记住并非十进制算术每次浮点运算可能导致错误

虽然病态情况确实存在对于大多数正常浮点运算使用简单最终显示结果期望十进制数值即可得到期望结果。 str() 通常足够对于精度控制参看 格式字符串语法 str.format() 方法格式描述

对于需要精确十进制表示使用场景尝试使用 decimal 模块模块实现适合会计应用高精度应用十进制运算

一种形式精确运算 fractions 模块提供支持模块实现基于有理数算术运算因此可以精确表示 1/3 这样数值)。

如果浮点运算重度用户那么应当了解一下 NumPy 以及 SciPy 项目提供许多其他数学统计运算参见 <https://scipy.org>。

Python
提供一些工具可能 确实 想要知道浮点数精确少数情况提供帮助例如 float.as_integer_ratio() 方法浮点数表示分数:
>>>

x = 3.14159

x.as_integer_ratio()
(3537115888337719, 1125899906842624)

由于这个比值精确可以用来无损重建原始:
>>>

x == 3537115888337719 / 1125899906842624
True

float.hex()
方法十六进制 16 基数表示浮点数同样存在计算机中的精确:
>>>

x.hex()
'0x1.921f9f01b866ep+1'

这种精确十六进制表示形式用来精确重建浮点数值:
>>>

x == float.fromhex('0x1.921f9f01b866ep+1')
True

由于这种表示精确适用跨越不同版本平台无关 Python 移植数值以及支持相同格式其他语言例如 Java C99)交换数据.

另一有用工具 sum() 函数能够帮助减少求和过程中的精度损失数值添加总计时候中间步骤使用扩展精度可以保持总体精确度使得错误不会积累能够影响最终总计程度:
>>>

0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
False

sum([0.1] * 10) == 1.0
True

math.fsum()
函数进一步追踪过程丢失数位”,因此结果经过一次相比 sum() 执行速度一些常见情况更加准确尤其是数值输入彼此几乎相互抵消最终结果接近零时
>>>

arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,

-143401161400469.7, 266262841.31058735, -0.003244936839808227]

float(sum(map(Fraction, arr))) #
精确求和结果经过一次四舍五入
8.042173697819788e-13

math.fsum(arr) #
一次四舍五入
8.042173697819788e-13

sum(arr) #
多次四舍五入扩展精度
8.042178034628478e-13

total = 0.0

for x in arr:

total += x #
多次四舍五入标准精度


total #
直接加法没有正确数字
-0.0051575902860057365

15.1.
表示错误

小节详细解释 "0.1" 例子说明可以怎样亲自此类情况进行精确分析假定前提基本熟悉二进制浮点表示

表示错误 某些其实大多数十进制小数无法二进制 2 基数计数精确表示事实造成错误就是为什么 Python(或者 Perl、C、C++、Java、Fortran 以及许多其他语言经常不会显示期待精确十进制数值主要原因

为什么这样? 1/10 无法二进制小数精确表示至少 2000 几乎所有机器使用 IEEE 754 二进制浮点运算标准几乎所有系统平台 Python 浮点数映射 IEEE 754 binary64 "精度" 。 IEEE 754 binary64 包含 53 精度因此输入计算机尽量 0.1 转换 J/2**N 形式所能表示接近小数其中 J 恰好包含 53 比特整数重新

1 / 10 ~= J / (2**N)



J ~= 2**N / 10

并且由于 J 恰好 53 ( >= 2**52 < 2**53),N 最佳 56:
>>>

2**52 <= 2**56 // 10 < 2**53
True

也就是说,56 唯一使 J 恰好 53 N 这样 J 可能最佳就是之后:
>>>

q, r = divmod(2**56, 10)

r
6

由于余数 10 一半所以最佳近似通过向上获得:
>>>

q+1
7205759403792794

因此 IEEE 754 精度可能达到 1/10 最佳近似:

7205759403792794 / 2 ** 56

分子分母除以结果小数:

3602879701896397 / 2 ** 55

注意由于我们向上这个结果实际上大于 1/10;如果我们没有向上将会小于 1/10。 无论如何不会 精确 1/10!

因此计算机永远不会 "看到" 1/10: 实际看到就是上面小数达到最佳 IEEE 754 精度近似:
>>>

0.1 * 2 ** 55
3602879701896397.0

如果我们小数乘以 10**55,我们可以看到输出 55 十进制数位:
>>>

3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

意味着存储计算机中的确切数字等于十进制数值 0.1000000000000000055511151231257827021181583404541015625。 许多语言包括版本 Python)不会显示这个完整十进制数值而是结果 17 有效数字:
>>>

format(0.1, '.17f')
'0.10000000000000001'

fractions
decimal 模块使得这样计算更为容易:
>>>

from decimal import Decimal

from fractions import Fraction

Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)

(0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)

Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'