Python浮点算术

一、争议和限制

在计算机中,浮点数通常以二进制小数的形式表示,其中每个位代表一个不同的十进制数字。例如,一个 32 位的浮点数可以表示 23 个十进制数字,其中最高的 23 个位表示符号位,接下来的 8 个位表示指数,最后的 23 个位表示尾数。

当计算机处理浮点数时,它会将二进制小数转换为十进制数字进行计算。这个过程涉及到舍入和精度损失的问题,因为某些十进制小数无法精确地表示为二进制小数。例如,十进制小数 0.1 无法精确地表示为二进制小数,因此在计算机中它被近似为 0.0001100110011001100110011...。这种舍入和精度损失可能会导致一些计算错误,需要进行一些特殊的处理来避免这种情况的发生。

用十进制来理解这个问题显得更加容易一些。考虑分数 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') # 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:

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

对于需要精确十进制表示的使用场景,建议尝试使用 decimal 模块,该模块实现了适合会计应用和高精度应用的十进制运算。另一种形式的精确运算由 fractions 模块提供支持,该模块实现了基于有理数的算术运算(因此可以精确表示像 1/3 这样的数值)。

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))) # Exact summation with single rounding
8.042173697819788e-13
>>>math.fsum(arr) # Single rounding
8.042173697819788e-13
>>>sum(arr) # Multiple roundings in extended precision
8.042178034628478e-13
>>>total = 0.0
>>>for x in arr:
... total += x # Multiple roundings in standard precision
... 
>>>total # Straight addition has no correct digits!
-0.0051575902860057365

二、表示性错误

接下来将详细解释 "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'
广告合作
QQ群号:707632017

温馨提示:

1、本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com。(#改为@)

2、本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。

目录