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


158

我知道大多数小数都没有确切的浮点表示形式(浮点数学运算符是否损坏?)。

但是,当两个值实际上都具有丑陋的十进制表示形式时,我看不出为什么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')

7
因为有些数字可以准确表示,有些不能。
Morgan Thrapp

58
@MorganThrapp:不,不是。OP正在询问外观相当随意的格式选择。0.3和0.4都不能精确表示为二进制浮点数。
Bathsheba

42
@BartoszKP:看了文档几次,它没有解释为什么Python显示0.3000000000000000444089209850062616169452667236328125作为0.300000000000000040.40000000000000002220446049250313080847263336181640625作为.4,即使他们似乎有相同的精度,因此不回答这个问题。
Mooing Duck

6
另请参见stackoverflow.com/questions/28935257/…-我有点恼火,因为它作为副本被关闭了,但是没有。
Random832 '16

12
重新打开后,请不要关闭它,因为“浮点数学运算已损坏”的重复项
Antti Haapala

Answers:


301

简单的答案是因为3*0.1 != 0.3归因于量化(四舍五入)误差(而4*0.1 == 0.4乘以2的幂通常是“精确”运算)。

您可以.hex在Python中使用该方法查看数字的内部表示形式(基本上是确切的二进制浮点值,而不是以10为底的近似值)。这可以帮助解释幕后情况。

>>> (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的float repr被设计为可双向访问的,也就是说,显示的值应完全可转换为原始值。因此,它无法显示0.30.1*3完全相同的方式,或两个不同的数字最终会往返后相同。因此,Python 3的repr引擎选择显示一个略有明显错误的引擎。


25
这是一个非常全面的答案,谢谢。(特别是,感谢您的展示.hex();我不知道它的存在。)
NPE

21
@supercat:Python会尝试找到最短的字符串,字符串会四舍五入为所需的值,无论发生什么情况。显然,评估值必须在0.5ulp以内(否则会四舍五入到其他值),但是在模棱两可的情况下可能需要更多位数。代码非常粗糙,但是如果您想窥视一下
nneonneo 2016年

2
@supercat:始终是0.5 ulp以内的最短字符串。(严格来说,如果我们要查看具有奇数LSB的浮点数;即,使它与平整度为偶数的最短字符串)。对此的任何例外都是错误,应予以报告。
Mark Dickinson

7
@MarkRansom当然,他们确实使用了其他东西,e因为那已经是一个十六进制数字。也许是p为了权力而不是指数
Bergi

11
@Bergi:p在此上下文中的使用(至少)可以追溯到C99,并且还出现在IEEE 754和各种其他语言(包括Java)中。当float.hexfloat.fromhex实现时(由我:-),Python只是复制了当时的惯例。我不知道意图是否是“ Power”的“ p”,但这似乎是思考它的好方法。
马克·迪金森

75

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

另一方面,乘法4*0.1 的确获得了最接近0.4的值(十六进制为0x1.999999999999ap-2),因此不需要任何其他数字。

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

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

我在上面使用了十六进制表示法,因为它既美观又紧凑,并且显示了两个值之间的位差。您可以使用eg自己执行此操作(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')

2
(+1)好的答案,谢谢。你认为它可能是值得说明的,包括结果的“不最接近的值”点3*0.1 == 0.34*0.1 == 0.4
NPE

@NPE,我应该马上做,谢谢你的建议。
Mark Ransom

我想知道是否值得注意最接近的“双精度数”精确到0.1、0.3和0.4的十进制值,因为很多人看不懂浮点十六进制。
超级猫

@supercat你说的很对。将那些超级大的双精度词放在文本中会分散注意力,但是我想到了一种添加它们的方法。
Mark Ransom

25

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

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

从3.2版开始,Python strrepr使用复杂的舍入机制,其更喜欢好看的小数,如果有可能,但使用更多的数字在需要保证双射(一个一对一)映射花车和它们的字符串表示之间。

这种方案保证repr(float(s))即使简单的小数点不能精确地表示为浮点数(例如when),其值对于简单的小数点也看起来不错s = "0.1")

同时,它保证float(repr(x)) == x每个浮动都成立x


2
您的答案对于> = 3.2的Python版本是正确的,其中strrepr对于浮点数是相同的。对于Python 2.7,repr具有您可以识别的属性,但str要简单得多-它仅计算12个有效数字并根据这些数字生成输出字符串。对于Python <= 2.6,两者reprstr均基于固定数量的有效数字(17表示repr,12表示str)。(而且没有人关心Python 3.0或Python 3.1 :-)
Mark Dickinson

谢谢@MarkDickinson!我在回答中包括了您的评论。
艾瓦尔(Aivar)2016年

2
请注意,从shell取整来自reprPython 2.7的行为将是相同的……
Antti Haapala

5

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

浮点数本质上是一个二进制数,但以科学计数法表示,有效数字的固定限制。

具有不与底数共享的质数因子的任何数字的逆将始终导致重复的点表示。例如1/7的素数为7,与10不共享,因此具有重复的十进制表示形式,素数为2和5的1/10也是如此,后者不与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 sigfig模式标识为十进制1.6,则它将适用于0.1、0.2、0.4、0.8等。对于其他组合(例如,浮点数3除以浮点数10),它也可能具有魔术的sigfig模式。以及其他统计上可能由10除以形成的魔术图案。

在3 * 0.1的情况下,最后几个有效数字可能与将浮点数3除以浮点数10有所不同,从而导致算法无法根据其对精度损失的容忍度来识别0.3常数的幻数。

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

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

对于精度损失没有容忍度,如果float x(0.3)不完全等于float y(0.1 * 3),则repr(x)不完全等于repr(y)。


4
这并不能真正增加现有答案。
Antti Haapala

1
“根据所使用的算法,它可能会尝试仅根据有效数字来猜测常见的小数。” <-这似乎是纯粹的猜测。其他答案描述了Python的实际功能。
Mark Dickinson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.