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

Why does the floating-point value of 4*0.1 look nice in Python 3 but 3*0.1 doesn't?

我知道大多数小数没有精确的浮点表示法 (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')

repr(和 Python 3 中的 str)将根据需要输出尽可能多的数字以使值明确。在这种情况下,乘法 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

我在上面使用了十六进制表示法,因为它既漂亮又紧凑,并且显示了两个值之间的位差。您可以自己使用例如(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 方法来查看数字的内部表示(基本上,exact 二进制浮点值,而不是比以 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的浮点数repr被设计为round-trippable,即显示的值应该可以准确转换为原始值( float(repr(f)) == f 所有浮点数 f)。因此,它不能以完全相同的方式显示 0.30.1*3,否则两个 不同的 数字在往返后最终会相同。因此,Python 3 的 repr 引擎选择显示一个略微明显的错误。

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

If you check a float on Python's command line or print it, it goes through function repr which creates its string representation.

Starting with version 3.2, Python's str and repr use a complex rounding scheme, which prefers nice-looking decimals if possible, but uses more digits where necessary to guarantee bijective (one-to-one) mapping between floats and their string representations.

This scheme guarantees that value of repr(float(s)) looks nice for simple decimals, even if they can't be represented precisely as floats (eg. when s = "0.1").

At the same time it guarantees that float(repr(x)) == x holds for every float x

并非真正特定于 Python 的实现,但应适用于任何浮点数到十进制字符串函数。

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

任何具有不与基数共享的素数因子的数字的倒数将始终导致重复出现的点点表示。例如 1/7 有一个质因数 7,它不与 10 共享,因此有一个循环的十进制表示,同样适用于 1/10 的质因数 2 和 5,后者不与 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 模式,例如浮点数 3 除以浮点数 10和其他在统计上可能由除以 10 形成的魔术图案。

在 3*0.1 的情况下,最后几个有效数字可能与将浮点数 3 除以浮点数 10 不同,导致算法无法识别 0.3 常量的幻数,具体取决于其容忍度精度损失。

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

Interestingly, there are many different decimal numbers that share the same nearest approximate binary fraction. For example, the numbers 0.1 and 0.10000000000000001 and 0.1000000000000000055511151231257827021181583404541015625 are all approximated by 3602879701896397 / 2 ** 55. Since all of these decimal values share the same approximation, any one of them could be displayed while still preserving the invariant eval(repr(x)) == x.

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