为什么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')
16465 次浏览

repr(以及 Python3中的 str)将输出所需的数字,以使该值明确无误。在这种情况下,乘法 3*0.1的结果并不是最接近0.3的值(0x1.33333333333333333333333p-2,十六进制) ,它实际上比0.3高一个 LSB (0x1.3333333333333333333333334p-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')

答案很简单,因为 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.9999999999999 a 乘以2 ^ -4。末尾的“ a”表示数字10——换句话说,二进制浮点数中的0.1比0.1的“精确”值大(因为最终的0x0.99被四舍五入为0x0)。答)。当你把这个数乘以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被设计为 往返的,也就是说,所显示的值应该可以精确地转换为原始值(对于所有的 float ffloat(repr(f)) == f)。因此,它不能以完全相同的方式显示 0.30.1*3,否则两个 与众不同号码在往返后将以相同的结果结束。因此,Python3的 repr引擎选择显示一个略有明显错误的引擎。

下面是从其他答案中得出的一个简化的结论。

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

从3.2版本开始,Python 的 strrepr使用了一种复杂的舍入方案 如果可能的话,漂亮的小数,但使用更多的数字 需要保证浮点数之间的双射(一对一)映射 以及它们的字符串表示形式。

这个方案保证了 repr(float(s))的值看起来很简单 小数,即使它们不可能是 精确地表示为浮点数(例如,当 s = "0.1")

同时,它保证每个浮点 xfloat(repr(x)) == x保持不变

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

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

任何具有素数因子但不与基数共享的数字的反数总是会导致重复出现的点点表示。例如1/7有一个素因子7,它不与10共享,因此有一个循环小数,对于1/10有素因子2和5也是如此,后者不与2共享,这意味着0.1不能精确地表示为点点后的有限位数。

由于0.1没有精确的表示,所以将近似值转换为小数点字符串的函数通常会尝试近似某些值,以避免得到像0.100000000004121这样不直观的结果。

由于浮点数是科学记数法的,因此任何乘以基数幂的运算都只会影响数字的指数部分。例如1.231 e + 2 * 100 = 1.231 e + 4(十进制) ,同样,1.00101010 e11 * 100 = 1.001010 e101(二进制)。如果我乘以基数的非幂,有效数字也会受到影响。例如1.2 e1 * 3 = 3.6 e1

根据所使用的算法,它可能只根据有意义的数字来猜测常见的小数。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,以及统计上可能由除以10形成的其他魔术模式。

在3 * 0.1的情况下,最后几个有意义的数字可能不同于将浮点数3除以浮点数10,这会导致算法无法识别0.3常量的神奇数字,这取决于它对精度损失的容忍度。

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

有趣的是,有许多不同的十进制数共享相同的近似二进制分数。例如,数字0.1和0.1000000000000000000000001和0.10000000000005551151231257827021181583404541015625都是由3602879701896397/2 * * 55近似的。由于所有这些十进制值都具有相同的近似值,所以它们中的任何一个都可以在保留不变 eval (repr (x)) = = x 的情况下显示出来。

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