为什么 4*0.1 的浮点值在 Python 3 中看起来不错,而 3*0.1 却不好看?
- 2025-04-16 08:56:00
- admin 原创
- 15
问题描述:
我知道大多数小数没有精确的浮点表示(浮点数学有问题吗?)。
但我不明白为什么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')
解决方案 1:
简单的答案是由于3*0.1 != 0.3
量化(舍入)误差(而4*0.1 == 0.4
乘以2的幂通常是“精确”运算)。Python 会尝试找到能够舍入到所需值的最短字符串,因此它可以显示4*0.1
为0.4
“因为这些相等”,但不能显示3*0.1
为“0.3
因为这些不相等”。
您可以使用.hex
Python 中的方法来查看数字的内部表示(基本上是精确的二进制浮点值,而不是十进制的近似值)。这有助于解释底层发生的事情。
>>> (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.13 比四舍五入后的数值 0.3 略大。*
Python 3 的浮点数repr
被设计为可转换的,也就是说,显示的值应该能够精确地转换为原始值(float(repr(f)) == f
对于所有浮点数而言f
)。因此,它无法以完全相同的方式显示0.3
和,否则两个不同的数字在转换后会变成相同的结果。因此,Python 3 的引擎选择显示一个略带明显错误的值。0.1*3
`repr`
解决方案 2:
repr
(在 Python 3 中也是如此str
)会根据需要输出尽可能多的数字,以确保值无歧义。在本例中,乘法的结果3*0.1
不是最接近 0.3 的值(十六进制为 0x1.333333333333p-2),实际上它比 0.3 低一位(0x1.333333333334p-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:
这是从其他答案中得出的简化结论。
如果您在 Python 的命令行上检查浮点数或打印它,它会通过
repr
创建其字符串表示形式的函数。从 3.2 版开始,Python 的
str
和repr
使用复杂的舍入方案,该方案尽可能采用美观的小数,但在必要时使用更多数字来保证浮点数和它们的字符串表示之间的双射(一对一)映射。这种方案保证了 的值
repr(float(s))
对于简单的小数来说看起来很好看,即使它们不能精确地表示为浮点数(例如当 时s = "0.1")
)。同时它保证
float(repr(x)) == x
每个浮点数都成立x
解决方案 4:
并不是特定于 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 的有效数字模式识别为小数 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.10000000000000001 和 0.1000000000000000055511151231257827021181583404541015625 这几个数字,它们的近似值都是 3602879701896397 / 2 ** 55。由于所有这些十进制值都拥有相同的近似值,因此它们中的任何一个都可以显示,同时仍然保留不变式 eval(repr(x)) == x。
不允许有精度损失,如果 float x (0.3) 不完全等于 float y (0.1*3),那么 repr(x) 也不完全等于 repr(y)。
扫码咨询,免费领取项目管理大礼包!