Python 中浮点数常见问题说明

本教程是 Python 官方网站上 《Tutorial》 部分文档的翻译,本文档与 官方文档授权一致

1. 浮点数说明

浮点数在计算机硬件中表示为以 2 为基数(二进制)的小数。举例而言,十进制的小数

0.125

等于 1/10 + 2/100 + 5/1000,同理,二进制的小数

0.001

等于 0 /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.10.100000000000000010.1000000000000000055511151231257827021181583404541015625 全都近似于 3602879701896397 / 2 **** 55。由于所有这些十进制值都具有相同的近似值,因此可以显示其中任何一个,同时仍然保留不变的 eval(repr(x)) == x

在历史上,Python 提示符和内置的 repr() 函数会选择具有 17 位有效数字的来显示,即 0.10000000000000001。从 Python 3.1 开始,Python(在大多数系统上)现在能够选择这些表示中最短的并简单地显示 0.1

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

想要更美观的输出,你可能会希望使用字符串格式化来产生限定长度的有效位数:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'

>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'

>>> repr(math.pi)
'3.141592653589793'

必须重点了解的是,这在实际上只是一个假象:你只是将真正的机器码值进行了舍入操作再 显示 而已。

一个假象还可能导致另一个假象。例如,由于这个 0.1 并非真正的 1/10,将三个 0.1 的值相加也不一定能恰好得到 0.3:

>>> .1 + .1 + .1 == .3
False

而且,由于这个 0.1 无法精确表示 1/10 的值而这个 0.3 也无法精确表示 3/10 的值,使用 round() 函数进行预先舍入也是没用的:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

虽然这些小数无法精确表示其所要代表的实际值,round() 函数还是可以用来“事后舍入”,使得实际的结果值可以做相互比较:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

二进制浮点运算会造成许多这样的“意外”。有关 "0.1" 的问题会在下面的“表示性错误”一节中更详细地描述。请参阅 浮点数的危险性 一文了解有关其他常见意外现象的更详细介绍。

正如那篇文章的结尾所言,“对此问题并无简单的答案。”但是也不必过于担心浮点数的问题!Python 浮点运算中的错误是从浮点运算硬件继承而来,而在大多数机器上每次浮点运算得到的 2****53 数码位都会被作为 1 个整体来处理。这对大多数任务来说都已足够,但你确实需要记住它并非十进制算术,且每次浮点运算都可能会导致新的舍入错误。

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

  • 发表于 · 2022.04.13 09:35 · 阅读 · 1870

[版权声明] :本文文字、代码及图片版权归原作者所有,任何媒体、网站或个人未经本网协议授权不得采集、整理、转载或以其他方式复制发表。已经本站协议授权的媒体、网站,在使用时必须注明“稿件来源:学研谷”。

0 条评论

请先 登录 后评论
猜猜我是谁
traveler

10
提问
12
回答
18
文章