为什么在Python 3中,4*0.1的浮点数值看起来好看,而3*0.1则不是呢?

163

我知道大多数十进制数没有精确的浮点表示(浮点数运算是否有问题?)。

但是,当两个值都有丑陋的小数表示形式时,我不明白为什么4*0.1可以漂亮地打印为0.4,而3*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')

59
@MorganThrapp:不是这样的。楼主询问的是看起来相当随意的格式选择。无论是0.3还是0.4都无法在二进制浮点数中被精确表示。 - Bathsheba
4
每个与浮点数相关的问题下必须提供以下链接:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html - BartoszKP
43
阅读了这份文件多次后,它并没有解释为什么Python将0.3000000000000000444089209850062616169452667236328125显示为0.30000000000000004,将0.40000000000000002220446049250313080847263336181640625显示为.4,尽管它们似乎具有相同的精度,因此没有回答这个问题。 - Mooing Duck
6
请参见http://stackoverflow.com/questions/28935257/why-0-4-2-equals-to-0-2-meanwhile-0-6-3-equals-to-0-19999999999999998-in - 我有点烦恼,因为它被关闭为重复问题,但这个问题却没有。 - Random832
15
重新打开,请勿将此关闭为“浮点运算是否有问题”的副本。 - Antti Haapala -- Слава Україні
显示剩余5条评论
4个回答

310

简单的答案是由于量化(舍入)误差导致 3*0.1 != 0.3 (而 4*0.1 == 0.4 因为乘以二的幂次方通常是一个“精确”的操作)。Python 尝试找到可舍入为期望值的最短字符串 ,所以它可以将4*0.1 显示为0.4,因为这两者相等,但它无法将3*0.1 显示为 0.3,因为这两者不相等。

您可以在 Python 中使用 .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)。 当您将其乘以一个是2的幂的数字 4 时,指数会向上移动(从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 的 float repr 被设计为可以往返转换,也就是说,显示的值应该可以精确地转换回原始值(对于所有的浮点数 f,都满足 float(repr(f)) == f)。因此,它无法以完全相同的方式显示 0.30.1 * 3,否则这两个不同的数字在经过往返转换后将变得相同。因此,Python 3 的 repr 引擎选择显示其中一个具有轻微的明显误差。


25
非常感谢您的细致回答,特别是感谢您展示了 .hex() 方法;我之前并不知道它的存在。 - NPE
22
Python试图找到最短的字符串,可以四舍五入为所需的值,不管它是什么。显然,计算出的值必须在0.5ulp范围内(否则会四舍五入为其他值),但在模棱两可的情况下可能需要更多数字。代码非常复杂,但如果您想偷看一下,请访问以下链接:https://hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345 - nneonneo
2
@supercat:始终是在0.5 ulp范围内的最短字符串。如果我们正在查看具有奇数LSB的浮点数(即,使其使用round-ties-to-even工作的最短字符串),则严格在内部。任何例外都是错误,并应报告。 - Mark Dickinson
7
@MarkRansom 肯定他们使用了除 e 以外的其他字符,因为它已经是一个十六进制数字。也许使用 p 来代替 指数 会更好些。 - Bergi
12
在这种情况下使用p至少可以追溯到C99,也出现在IEEE 754和其他各种语言中(包括Java)。当float.hexfloat.fromhex被实现时(由我实现:-),Python只是在复制当时已经确立的惯例。我不知道使用p的意图是否为“Power”,但这似乎是一个不错的思考方式。 - Mark Dickinson
显示剩余8条评论

77

repr函数(在Python 3中也适用于str)会输出足够的位数使得值不会产生歧义。在本例中,乘法3*0.1的结果并非最接近0.3的值(16进制为0x1.3333333333333p-2),实际上比最接近的值高一个LSB(0x1.3333333333334p-2),因此需要更多位数来区分它与0.3。

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

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

>>> 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.1、0.3和0.4的“双精度浮点数”的精确十进制值,因为很多人无法读取浮点十六进制。 - supercat
@supercat 你说得很有道理。把那些超大的双精度数放到文本中会很分散注意力,但我想到了一种添加它们的方法。 - Mark Ransom

26

这是其他答案的简化结论。

如果你在 Python 命令行或使用 print 输出一个浮点数,它会经过函数 repr 处理,生成字符串表示。

从版本 3.2 开始,Python 的 str 和 repr 使用了复杂的舍入方案,偏向于使用美观易读的小数位,但也会在需要时使用更多数字以保证浮点数与它们的字符串表示之间的一对一映射关系。

该方案保证了当使用 repr(float(s)) 对简单小数进行处理时,其输出结果看起来很好,即使它们不能精确地表示为浮点数(例如,当 s = "0.1" 时)。

同时,它保证对于任何浮点数 x,float(repr(x)) == x 均成立。


3
对于Python版本大于等于3.2,您的答案是准确的,其中strrepr在浮点数上是相同的。对于Python 2.7,repr具有您所确定的特性,但str要简单得多-它只计算12个有效数字,并基于这些数字生成输出字符串。对于Python 2.6及以下版本,reprstr都基于固定数量的有效数字(repr为17位,str为12位)。(而且没有人关心Python 3.0或Python 3.1 :-)) - Mark Dickinson
谢谢@MarkDickinson!我已经在答案中包含了您的评论。 - Aivar
2
请注意,来自shell的四舍五入是来自repr,因此Python 2.7的行为将是相同的... - Antti Haapala -- Слава Україні

5
并不只是Python实现的问题,适用于任何将浮点数转为十进制字符串的函数。
浮点数本质上是一个二进制数,但采用科学计数法表示,并具有固定数量的有效数字。
任何具有质数因子且不与底数共享的数字的倒数总会产生一个循环小数。例如,1/7具有质数因子7,它不与10共享,因此具有循环小数表示形式;同样的,1/10具有质数因子2和5,后者不与2共享。这意味着0.1无法通过有限数量的二进制位来精确表示。
由于0.1没有精确表示,将其近似为十进制字符串的函数通常会尝试近似某些值,以避免出现像0.1000000000004121这样的不可理解的结果。
由于浮点数采用科学计数法表示,因此乘以基数的幂仅会影响数字的指数部分。例如,在十进制表示中,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的有效数字模式识别为十进制数1.6,则它将适用于0.1、0.2、0.4、0.8等。它还可以针对其他组合具有神奇的有效数字模式,例如除以10得到的float 3和float 10的浮点数模式。
在3*0.1的情况下,最后几个有效数字可能与通过float 3除以float 10得到的0.3常数不同,这取决于它对精度损失的容忍程度,从而导致算法无法识别0.3常数的神奇数字。
编辑: https://docs.python.org/3.1/tutorial/floatingpoint.html 有趣的是,许多不同的十进制数可以共用一个最近似的二进制分数。例如,数字0.1、0.10000000000000001和0.1000000000000000055511151231257827021181583404541015625都可以近似表示为3602879701896397 / 2 ** 55。由于所有这些十进制值都共用同一近似值,因此任何一个数都可以被显示,同时仍保持invariant eval(repr(x)) == x。

精度损失是无法容忍的,如果浮点数x(0.3)不等于浮点数y(0.1*3),那么repr(x)就不完全等于repr(y)。


4
这并没有为现有的回答增添太多内容。 - Antti Haapala -- Слава Україні
1
根据所使用的算法,它可能仅基于有效数字尝试猜测常见的小数。这似乎是纯粹的推测。其他答案已经描述了Python实际上做了什么。 - Mark Dickinson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接