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

时间:2016-09-21 14:07:21

标签: python floating-point rounding floating-accuracy ieee-754

我知道大多数小数都没有精确的浮点表示(Is floating point math broken?)。

但我不明白为什么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')

4 个答案:

答案 0 :(得分:297)

简单的答案是因为3*0.1 != 0.3由于量化(舍入)错误(而4*0.1 == 0.4因为乘以2的幂通常是“精确”操作。)

您可以使用Python中的.hex方法查看数字的内部表示(基本上是精确二进制浮点值,而不是基数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被设计为 round-trippable ,也就是说,显示的值应该可以完全转换为原始值。因此,它无法以完全相同的方式显示0.30.1*3,或者在往返后两个不同的数字最终会相同。因此,Python 3的repr引擎选择显示一个有轻微明显错误的引擎。

答案 1 :(得分:75)

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

另一方面,乘法4*0.1 得到最接近的值0.4(十六进制为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')

答案 2 :(得分:22)

以下是其他答案的简化结论。

  

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

     

从版本3.2开始,Python的strrepr使用复杂的舍入方案,它更喜欢   如果可能,看起来很漂亮的小数,但在其中使用更多的数字   必须保证浮动之间的双向(一对一)映射   和他们的字符串表示。

     

这个方案保证repr(float(s))的值看起来很简单   小数,即使它们不能   恰好表示为浮点数(例如,当s = "0.1")时。

     

同时它保证float(repr(x)) == x适用于每个浮动x

答案 3 :(得分: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等上工作。它也可能具有其他组合的魔法sigfig模式,例如float 3除以float 10和统计上可能通过除以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。

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