【问题标题】:Why does the floating-point value of 4*0.1 look nice in Python 3 but 3*0.1 doesn't?为什么 4*0.1 的浮点值在 Python 3 中看起来不错,但 3*0.1 不好看?
【发布时间】:2017-01-29 20:16:57
【问题描述】:

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

【问题讨论】:

  • 因为有些数字可以准确表示,有些则不能。
  • @MorganThrapp:不,不是。 OP 正在询问看起来相当随意的格式选择。 0.3 和 0.4 都不能用二进制浮点数精确表示。
  • @BartoszKP:多次阅读该文档后,它无法解释为什么 Python 将 0.3000000000000000444089209850062616169452667236328125 显示为 0.300000000000000040.40000000000000002220446049250313080847263336181640625 显示为 .4,即使它们看起来相同准确性,因此无法回答问题。
  • 另请参阅stackoverflow.com/questions/28935257/… - 我有点恼火它被关闭为重复但这个没有。
  • 已重新打开,请不要将其关闭,因为它与“浮点数学是否损坏”的重复

标签: python floating-point rounding floating-accuracy ieee-754


【解决方案1】:

简单的答案是因为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.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 引擎选择显示一个带有轻微明显错误的引擎。

【讨论】:

  • 这是一个非常全面的答案,谢谢。 (特别感谢您显示.hex();我不知道它存在。)
  • @supercat:Python 试图找到可以四舍五入到所需值的最短字符串,不管它是什么。显然,评估值必须在 0.5ulp 以内(否则会四舍五入),但在模棱两可的情况下可能需要更多数字。代码非常很粗糙,但如果你想看一看:hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
  • @supercat:总是在 0.5 ulp 以内的最短字符串。 (如果我们正在查看具有奇数 LSB 的浮点数,则严格在其中;即,使其适用于圆结到偶数的最短字符串)。对此的任何异常都是错误,应报告。
  • @MarkRansom 当然,他们确实使用了 e 以外的其他东西,因为那已经是一个十六进制数字。也许p 代表 power 而不是 exponent
  • @Bergi:p 在这种情况下的使用可以追溯到(至少)C99,也出现在 IEEE 754 和各种其他语言(包括 Java)中。当float.hexfloat.fromhex 被实现时(由我实现:-),Python 只是在复制当时已建立的实践。我不知道意图是否是“权力”的“p”,但这似乎是一种很好的思考方式。
【解决方案2】:

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')

【讨论】:

  • (+1) 很好的答案,谢谢。您认为通过包含3*0.1 == 0.34*0.1 == 0.4 的结果来说明“不是最接近的值”点是否值得?
  • @NPE 我应该一开始就这样做,谢谢你的建议。
  • 我想知道最接近 0.1、0.3 和 0.4 的“双精度”的精确十进制值是否值得注意,因为很多人看不懂浮点十六进制。跨度>
  • @supercat 你说得很好。将那些超大的双打放在文本中会分散注意力,但我想了一种方法来添加它们。
【解决方案3】:

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

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

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

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

同时它保证float(repr(x)) == x对于每个浮点数x都成立

【讨论】:

  • 您的答案对于 Python 版本 >= 3.2 是准确的,其中 strrepr 对于浮点数是相同的。对于 Python 2.7,repr 具有您识别的属性,但 str 更简单 - 它只计算 12 位有效数字并基于这些数字生成输出字符串。对于 Python repr 和 str 均基于固定数量的有效数字(repr 为 17,str 为 12)。 (而且没有人关心 Python 3.0 或 Python 3.1 :-)
  • 谢谢@MarkDickinson!我在答案中包含了您的评论。
  • 请注意,shell 的舍入来自repr,因此 Python 2.7 的行为将是相同的...
【解决方案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 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都由2分之3602879701896397** 55.近似由于所有这些十进制值的共享相同的近似,可以同时仍保持不变的eval(再版(X来显示它们中的任何一个) ) == x。

没有精度损失的容忍度,如果float x(0.3)不完全等于float y(0.1*3),那么repr(x)不完全等于repr(y)。

【讨论】:

  • 这并没有真正为现有答案添加太多内容。
  • "根据所使用的算法,它可能会尝试仅根据有效数字猜测常见的小数。" 实际上 做了什么。
猜你喜欢
  • 2012-01-26
  • 1970-01-01
  • 1970-01-01
  • 2021-04-21
  • 1970-01-01
  • 2015-05-09
  • 1970-01-01
  • 1970-01-01
  • 2018-06-30
相关资源
最近更新 更多