为什么 4*0.1 的浮点值在 Python 3 中看起来不错,但 3*0.1 却不然?

Why does the floating-point value of 4*0.1 look nice in Python 3 but 3*0.1 doesn't?

提问人:Aivar 提问时间:9/21/2016 最后编辑:CommunityAivar 更新时间:9/7/2020 访问量:17347

问:

我知道大多数小数没有精确的浮点表示(浮点数学坏了吗?

但我不明白为什么打印得很好,但不是,当 这两个值实际上都有丑陋的十进制表示:4*0.10.43*0.1

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
Python 舍入 浮点精度 IEEE-754

评论

59赞 Bathsheba 9/21/2016
@MorganThrapp:不,不是。OP 询问的是看起来相当随意的格式选择。0.3 和 0.4 都不能精确地表示为二进制浮点数。
4赞 BartoszKP 9/21/2016
每个浮点相关问题下的必填链接:docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
43赞 Mooing Duck 9/22/2016
@BartoszKP:在阅读了好几遍文档后,它没有解释为什么 Python 显示为 as 和 as,即使它们看起来具有相同的准确性,因此没有回答这个问题。0.30000000000000004440892098500626161694526672363281250.300000000000000040.40000000000000002220446049250313080847263336181640625.4
6赞 Random832 9/22/2016
Смотритетакже: stackoverflow.com/questions/28935257/... - 我有点恼火,它被关闭了,但这个没有。
15赞 Antti Haapala -- Слава Україні 9/24/2016
重新打开,请不要将其作为“浮点数学是否损坏”的副本关闭

答:

77赞 Mark Ransom 9/21/2016 #1

repr(在 Python 3 中)将根据需要输出尽可能多的数字,以使值明确。在这种情况下,乘法的结果不是最接近 0.3 的值(十六进制0x1.33333333333333p-2),它实际上高出一个 LSB(0x1.333333333333334p-2),因此它需要更多的数字才能将其与 0.3 区分开来。str3*0.1

另一方面,乘法确实得到最接近 0.4 的值(十六进制0x1.99999999999ap-2),因此它不需要任何额外的数字。4*0.1

你可以很容易地验证这一点:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

我在上面使用了十六进制表示法,因为它既漂亮又紧凑,并显示了两个值之间的位差。您可以使用例如 自行执行此操作。如果你更愿意看到他们所有的十进制荣耀,这里是:(3*0.1).hex()

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

评论

0赞 supercat 9/21/2016
我想知道是否值得注意最接近的“双精度”的精确十进制值为 0.1、0.3 和 0.4,因为很多人无法读取浮点十六进制。
0赞 Mark Ransom 9/21/2016
@supercat你说得很对。将这些超大的双打放入文本中会分散注意力,但我想到了一种方法来添加它们。
310赞 nneonneo 9/21/2016 #2

简单的答案是因为由于量化(舍入)误差(而因为乘以 2 的幂通常是“精确”的运算)。Python 试图找到四舍五入到所需值的最短字符串,因此它可以显示为,因为它们相等,但不能显示为,因为它们不相等。3*0.1 != 0.34*0.1 == 0.44*0.10.43*0.10.3

您可以使用 Python 中的方法查看数字的内部表示形式(基本上是精确的二进制浮点值,而不是以 10 为基数的近似值)。这有助于解释引擎盖下发生的事情。.hex

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1 是 0x1.999999999999a 乘以 2^-4。末尾的“a”表示数字 10 - 换句话说,二进制浮点数中的 0.1 比 0.1 的“精确”值大(因为最终的 0x0.99 四舍五入为 0x0.a)。当你把它乘以 4 时,即 2 的幂,指数向上移动(从 2^-4 到 2^-2),但数字在其他方面保持不变,所以 .4*0.1 == 0.4

但是,当您乘以 3 时,0x0.99 和 0x0.a0 (0x0.07) 之间的微小差异会放大成 0x0.15 的误差,该误差在最后一个位置显示为一位数错误。这导致 0.1*3 比舍入值 0.3 大。

Python 3 的浮点数被设计为可往返的,也就是说,显示的值应该完全可以转换为原始值(对于所有浮点数)。因此,它不能以完全相同的方式显示,否则两个不同的数字在往返后最终会相同。因此,Python 3 的引擎选择显示一个有轻微明显错误的引擎。reprfloat(repr(f)) == ff0.30.1*3repr

评论

25赞 NPE 9/21/2016
这是一个非常全面的答案,谢谢。(特别感谢您的展示;我不知道它的存在。.hex()
2赞 Mark Ransom 9/21/2016
@NPE您可能也感兴趣,但它会反过来。float.fromhex()
22赞 nneonneo 9/22/2016
@supercat:Python 试图找到四舍五入到所需值的最短字符串,无论它是什么。显然,评估值必须在 0.5ulp 以内(否则会四舍五入到其他值),但在模棱两可的情况下可能需要更多数字。代码非常粗糙,但如果你想看一看:hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
7赞 Bergi 9/22/2016
@MarkRansom 当然,他们确实使用了其他东西,而不是因为那已经是一个十六进制数字。也许是为了权力而不是指数ep
12赞 Mark Dickinson 9/22/2016
@Bergi:在这种情况下,这种使用至少可以追溯到 C99,并且也出现在 IEEE 754 和各种其他语言(包括 Java)中。当 和 被实现时(由我 :-),Python 只是复制了当时既定的做法。我不知道“权力”的意图是否是“p”,但这似乎是一种很好的思考方式。pfloat.hexfloat.fromhex
26赞 Aivar 9/22/2016 #3

这是从其他答案中得出的简化结论。

如果您在 Python 的命令行上检查浮点数或打印它,它会通过创建其字符串表示的函数。repr

从 3.2 版本开始,Python 的 和 使用复杂的舍入方案,它更喜欢 如果可能的话,漂亮的小数,但在 保证浮点数之间的双射(一对一)映射所必需的 及其字符串表示形式。strrepr

该方案保证了 对于简单来说,看起来不错的值 小数,即使它们不能 精确地表示为浮点数(例如,当 .repr(float(s))s = "0.1")

同时,它保证了每个浮动都保持float(repr(x)) == xx

评论

3赞 Mark Dickinson 9/22/2016
对于 Python 版本 >= 3.2,您的答案是准确的,其中 和 对于浮点数是相同的。对于 Python 2.7,具有您标识的属性,但要简单得多 - 它只计算 12 个有效数字并基于这些数字生成输出字符串。对于 Python <= 2.6,和 都基于固定数量的有效数字(17 表示,12 表示)。(没有人关心 Python 3.0 或 Python 3.1 :-)strreprreprstrreprstrreprstr
0赞 Aivar 9/22/2016
谢谢@MarkDickinson!我在答案中包含了您的评论。
2赞 Antti Haapala -- Слава Україні 9/24/2016
请注意,shell 的舍入来自,因此 Python 2.7 的行为是相同的......repr
5赞 AkariAkaori 9/22/2016 #4

并非真正特定于 Python 的实现,但应该适用于任何浮点数到十进制字符串函数。

浮点数本质上是一个二进制数,但在科学记数法中具有固定的有效数字限制。

任何具有不与基数共享的质数因子的数的倒数将始终导致重复的点表示。例如,1/7 有一个质因数 7,它不与 10 共享,因此具有重复的十进制表示,而质因数为 2 和 5 的 1/10 也是如此,后者不与 2 共享;这意味着 0.1 不能用点点之后的有限位数来精确表示。

由于 0.1 没有精确的表示,因此将近似值转换为小数点字符串的函数通常会尝试近似某些值,以便它们不会得到像 0.10000000000004121 这样的不直观结果。

由于浮点数是科学记数法,因此任何乘以基数的幂只会影响数字的指数部分。例如,1.231e+2 * 100 = 1.231e+4 表示十进制表示法,同样,1.00101010e11 * 100 = 1.00101010e101 表示二进制表示法。如果我乘以基数的非幂,有效数字也会受到影响。例如,1.2e1 * 3 = 3.6e1

根据所使用的算法,它可能会尝试仅根据有效数字来猜测常见的小数。0.1 和 0.4 在二进制中都具有相同的有效数字,因为它们的浮点数基本上分别是 (8/5)(2^-4) 和 (8/5)(2^-6) 的截断。如果算法将 8/5 sigfig 模式识别为十进制 1.6,那么它将在 0.1、0.2、0.4、0.8 等上工作。它也可能具有其他组合的魔术 sigfig 模式,例如浮点数 3 除以浮点数 10 和其他在统计上可能由除以 10 形成的魔术模式。

在 3*0.1 的情况下,最后几个有效数字可能与将浮点数 3 除以浮点数 10 不同,导致算法无法识别 0.3 常数的幻数,具体取决于其对精度损失的容差。

编辑:https://docs.python.org/3.1/tutorial/floatingpoint.html

有趣的是,有许多不同的十进制数共享相同的最接近的近似二进制分数。例如,数字 0.1 和 0.100000000000000001 和 0.10000000000000000055511151231257827021181583404541015625 都近似于 3602879701896397 / 2 ** 55。由于所有这些十进制值都具有相同的近似值,因此可以显示其中任何一个值,同时仍保留不变的 eval(repr(x)) == x。

精度损失没有容差,如果浮点数 x (0.3) 不完全等于浮点数 y (0.1*3),则 repr(x) 不完全等于 repr(y)。

评论

4赞 Antti Haapala -- Слава Україні 9/24/2016
这并没有真正增加现有答案。
1赞 Mark Dickinson 9/24/2016
“根据所使用的算法,它可能会尝试仅根据有效数字来猜测常见的小数。” <- 这似乎是纯粹的猜测。其他答案已经描述了 Python 的实际作用。