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



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

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

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

您可以在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"稍大;精确的";值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的浮点repr被设计为可往返,也就是说,所显示的值应该可以精确转换为原始值(所有浮点ffloat(repr(f)) == f(。因此,它不能以完全相同的方式显示0.30.1*3,否则两个不同的数字在往返后会相同。因此,Python3的repr引擎选择显示一个有轻微明显错误的引擎。

repr(以及Python 3中的str(将根据需要输出尽可能多的数字,以使值不含糊。在这种情况下,相乘的结果3*0.1不是最接近0.3的值(十六进制为0x1.3333333333p-2(,它实际上高出了一个LSB(0x1.33333333 4p-2(所以它需要更多的数字来将其与0.3区分开来。

另一方面,乘法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')

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

如果您在Python的命令行上检查或打印一个float,它将通过创建其字符串表示的函数repr

从3.2版本开始,Python的strrepr使用复杂的舍入方案如果可能的话,小数看起来不错,但在保证浮点之间的双射(一对一(映射所必需的以及它们的字符串表示。

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

同时保证float(repr(x)) == x对每个浮点x 保持

不是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。如果该算法将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.10000000000000001以及0.1000000000000000055511151231257827021181583404541015625都近似为3602879701896397/2**55。由于所有这些十进制值共享相同的近似值,因此它们中的任何一个都可以显示,同时仍然保持不变的eval(repr(x((==x。

精度损失没有公差,如果float x(0.3(不完全等于float y(0.1*3(,那么repr(x(也不完全等于repr(y(。

相关内容

  • 没有找到相关文章

最新更新