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

Aiv*_*var 154 python floating-point rounding floating-accuracy ieee-754

我知道大多数小数都没有精确的浮点表示(浮点数学是否被破坏?).

但我不明白为什么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')
Run Code Online (Sandbox Code Playgroud)

nne*_*neo 297

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

您可以使用.hexPython中的方法来查看数字的内部表示(基本上,是精确的二进制浮点值,而不是基数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'
Run Code Online (Sandbox Code Playgroud)

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引擎选择显示一个有轻微明显错误的引擎.

  • 这是一个非常全面的答案,谢谢.(特别感谢显示`.hex()`;我不知道它存在.) (23认同)
  • @supercat:Python试图找到*最短的字符串,它将四舍五入到所需的值*,无论发生什么情况.显然,评估值必须在0.5ulp之内(或者它会转向其他内容),但在模糊情况下可能需要更多数字.代码是*非常*gnarly,但是如果你想看一眼:https://hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345 (20认同)
  • @Bergi:在这种情况下使用`p`可以追溯到(至少)C99,也可以出现在IEEE 754和其他各种语言(包括Java)中.当实现`float.hex`和`float.fromhex`时(由我:-),Python只是复制了当时已建立的实践.我不知道"Power"的意图是"p",但它似乎是一种很好的思考方式. (10认同)
  • @MarkRansom当然他们确实使用了除了'e`以外的东西,因为那已经是十六进制数字了.也许'p`代表*power*而不是*exponent*. (7认同)
  • @NPE那么你可能也对“float.fromhex()”感兴趣,它的作用相反。 (2认同)
  • @supercat:始终是0.5 ulp以内的最短字符串.(*严格*如果我们正在查看具有奇数LSB的浮点数;即,最短的字符串使其与圆形连接到偶数).对此的任何例外都是错误,应该报告. (2认同)

Mar*_*som 75

repr(以及str在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
Run Code Online (Sandbox Code Playgroud)

我在上面使用了十六进制表示法,因为它很好而且紧凑,并且显示了两个值之间的位差.你可以自己使用例如(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')
Run Code Online (Sandbox Code Playgroud)

  • (+1)很好的回答,谢谢.您是否认为通过包含"3*0.1 == 0.3"和"4*0.1 == 0.4"的结果来说明"非最接近的值"可能是值得的? (2认同)

Aiv*_*var 22

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

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

从版本3.2开始,使用Python strrepr使用复杂的舍入方案,如果可能的话,它更喜欢漂亮的小数,但在必要时使用更多的数字来保证浮点数和它们的字符串表示之间的双向(一对一)映射.

这种方案保证repr(float(s))了简单小数的外观值很好,即使它们不能精确地表示为浮点数(例如s = "0.1").

同时它保证float(repr(x)) == x每次浮动的保留x

  • 你的答案对于Python版本> = 3.2是准确的,其中`str`和`repr`对于浮点数是相同的.对于Python 2.7,`repr`具有您识别的属性,但`str`更简单 - 它只是计算12位有效数字并根据这些数字生成输出字符串.对于Python <= 2.6,`repr`和`str`都基于固定数量的有效数字(对于`repr`为17,对于`str`为12).(没有人关心Python 3.0或Python 3.1 :-) (2认同)
  • 请注意,shell的舍入来自`repr`,因此Python 2.7的行为将是相同的...... (2认同)

Aka*_*ori 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.

如果float x(0.3)不完全等于float y(0.1*3),则不存在精度损失的容差,则repr(x)不完全等于repr(y).

  • 这并没有真正增加现有答案. (4认同)