Python 浮点运算:问题和限制

查看全部 Python3 基础教程

浮点数数制转换问题

浮点数在计算机硬件中表示为以 2 为基数(二进制)的小数。例如,十进制的小数 0.125 等于 1/10 + 2/100 + 5/1000,同理,二进制的小数 0.001 等于 0/2 + 0/4 + 1/8。这两个小数具有相同的值,唯一真正的区别是第一个是以 10 为基数的小数表示,第二个则是以 2 为基数。

不幸的是,大多数的十进制小数都不能精确地表示为二进制小数。这导致通常输入的十进制浮点数都只能近似地以二进制浮点数形式储存在计算机中。

先用十进制来理解这个问题会更加容易一些。考虑分数 1/3,可以得到它在十进制下的一个近似值 0.3,或者,更近似的 0.330.333 等等。无论写下多少的数字,结果都永远不会精确等于 1/3,只会是越来越接近 1/3

同样的道理,无论使用多少位以 2 为基数的数字,十进制的 0.1 都无法精确地表示为一个以 2 为基数的小数。在以 2 为基数的情况下,1/10 是一个无限循环小数 0.0001100110011001100110011001100110011001100110011...

在任何有限数量的位置停下,都能得到一个近似值。在今天的大多数机器上,浮点数都是用二进制分数近似地表示,分子使用从最高有效位开始的前 53 位,分母为 2 的幂。在 1/10 这个例子中,相应的二进制分数是 3602879701896397 / 2 ** 55,它接近但不完全等于 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() 函数进行预先舍入(pre-rounding)是没用的:

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

虽然这些数无法精确表示其所要代表的实际值,但 round() 函数可以用来事后舍入(post-rounding),使得拥有非精确值的结果之间可以相互比较:

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

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

Programming with the Perils
There are no easy answers. It is the nature of binary floating-point to behave the way I have described. In order to take advantage of the power of computer floating-point, you need to know its limitations and work within them. Keeping the following things in mind when programming floating-point arithmetic should help a lot:

  1. Only about 7 decimal digits are representable in single-precision IEEE format, and about 16 in double-precision IEEE format.
  2. Every time numbers are transferred from external decimal to internal binary or vice-versa, precision can be lost.
  3. Always use safe comparisons.
  4. Beware of additions and subtractions that can quickly erode the true significance in a result. The computer doesn’t know what bits are truely significant.
  5. Conversions between data types can be tricky. Conversions to double-precision don’t increase the number of truely significant bits. Conversions to integer always truncate toward zero, even if the floating-point number is printed as a larger integer.
  6. Don’t expect identical results from two different floating-point implementations.

    I hope that I have given you a little more awareness of what is happening in the internals of floating-point arithmetic, and that some of the strange results you have seen make a little more sense.

    While some of the “perils” can be avoided, many just need to be understood and accepted.

正如那篇文章的结尾所言,“对此问题并无简单的答案。” 但也不必过于警惕浮点数的问题。Python 浮点运算中的错误是从浮点硬件继承来的,且在大多数机器上每次运算误差不会超过 2**53 分之一。这对大多数任务来说已经足够了,但你需要记住它并非十进制算术,并且每次浮点运算都可能遇到新的舍入误差。

虽然病态的情况确实存在,但对于大部分随意使用浮点运算的情况来说,只需将最终结果的显示舍入为期望的小数位数,最终就能得到期望的结果。str() 通常就足够了,对于更精确的控制可以参考 Format String Syntaxstr.format() 方法的格式说明符。

对于需要精确十进制表示的使用场景,可以使用 decimal 模块,该模块实现了适合会计应用和高精度应用的十进制运算。

fractions 模块支持另一种形式的精确运算,该模块实现了基于有理数的算术(因此可以精确表示像 1/3 之类的数字)。

如果是浮点运算的重度用户,应该要看一下 SciPy 项目提供的 Numerical Python 包和许多其他用于数学和统计运算的包。

如果真想知道某个浮点数的精确值,Python 也为此提供了一些工具。例如 float.as_integer_ratio() 方法可以将浮点数表示为一个分数:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

由于比值是精确的,因此它可以被用来无损地重建原始值:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex() 方法可以用十六进制(基数)来表示浮点数,这同样是保存在计算机中的精确值:

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

这种精确的十六进制表示法可被用来精确地重建浮点值:

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

由于这种表示法是精确的,它能用于在不同 Python 版本(平台无关)之间可靠地移植数值,以及和支持相同格式的其他语言(例如 Java 和 C99)交换数据。

另一个有用的工具是 math.fsum() 函数,它有助于减轻求和过程中的精度损失。它会在数值被添加到总计值的时候跟踪“丢失的数位”,这可能会影响整体精度,使得误差不会累积到影响最终总数的程度:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

表示性错误

本节将详细解释 “0.1” 的例子,并展示如何对该类情况进行精确分析。假定已基本熟悉二进制浮点表示法。

表示性错误是指一些(实际上是大多数)十进制分数无法精确表示为二进制(基数为 2)分数。这就是为什么 Python(或者 Perl、C、C++、Java、Fortran 以及许多其他语言)经常不会显示所期望的精确十进制数值的主要原因。

为什么会这样?1/10 是无法用二进制小数精确表示的。目前(2000 年 11 月)几乎所有机器都使用 IEEE-754 浮点运算,几乎所有平台都将 Python 浮点数映射为 IEEE-754 “双精度”。754 双精度包含 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

所以在 754 双精度下 1/10 的最佳近似值为:7205759403792794 / 2 ** 56

将分子和分母都除以二把分数减少为:

3602879701896397 / 2 ** 55

注意由于做了向上舍入,因此该结果实际上略大于 1/10。如果没有做向上舍入,则商将会略小于 1/10。但无论如何它都不会是精确的 1/10

所以计算永远不会“看到” 1/10:它看到的只是上面给出的精确分数,它所能达到的最佳 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'

fractionsdecimal 模块使这些计算变得很容易:

>>> 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'




参考资料

Python3 Tutorial – Floating Point Arithmetic: Issues and Limitations
The Perils of Floating Point

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值