【问题标题】:Why is Ruby's Float#round behavior different than Python's?为什么 Ruby 的 Float#round 行为与 Python 的不同?
【发布时间】:2013-03-22 01:20:23
【问题描述】:

"Behavior of “round” function in Python" 观察到 Python 轮次浮动是这样的:

>>> round(0.45, 1)
0.5
>>> round(1.45, 1)
1.4
>>> round(2.45, 1)
2.5
>>> round(3.45, 1)
3.5
>>> round(4.45, 1)
4.5
>>> round(5.45, 1)
5.5
>>> round(6.45, 1)
6.5
>>> round(7.45, 1)
7.5
>>> round(8.45, 1)
8.4
>>> round(9.45, 1)
9.4

接受的答案证实这是由于浮点数的二进制表示不准确造成的,这完全符合逻辑。

假设 Ruby 的浮点数和 Python 的一样不准确,那么 Ruby 怎么会像人类一样在圆上浮动呢? Ruby 会作弊吗?

1.9.3p194 :009 > 0.upto(9) do |n|
1.9.3p194 :010 >     puts (n+0.45).round(1)
1.9.3p194 :011?>   end
0.5
1.5
2.5
3.5
4.5
5.5
6.5
7.5
8.5
9.5

【问题讨论】:

  • 这确实很奇怪。 printf "%.20f", 1.45 # => 1.449999999999999955591.45.round 1 # => 1.5
  • 你的意思是,为什么round(1.45,1)在Python中返回1.4,而1.45.round(1)在Ruby中返回1.5

标签: python ruby rounding floating-accuracy


【解决方案1】:

总结

两种实现都面临相同的issues surrounding binary floating point numbers。

Ruby 通过简单的运算(乘以 10、调整和截断)直接对浮点数进行运算。

Python 使用 David Gay 的复杂算法将二进制浮点数转换为字符串,该算法产生与二进制浮点数完全相等的最短十进制表示。这不会做任何额外的舍入,它是一个精确的字符串转换。

有了最短的字符串表示,Python 使用精确的字符串运算四舍五入到适当的小数位数。浮点到字符串转换的目标是尝试“撤消”一些二进制浮点表示错误(即,如果您输入 6.6,Python 会在 6.6 上舍入,而不是 6.5999999999999996。

此外,Ruby 在舍入模式方面与某些版本的 Python 不同:从零舍入与对半偶数舍入。

详情

Ruby 不会作弊。它以与 Python 相同的普通旧二进制浮点数开始。因此,它面临一些相同的挑战(例如 3.35 表示略超过 3.35,而 4.35 表示略低于 4.35):

>>> Decimal.from_float(3.35)
Decimal('3.350000000000000088817841970012523233890533447265625')
>>> Decimal.from_float(4.35)
Decimal('4.3499999999999996447286321199499070644378662109375')

查看实现差异的最佳方法是查看底层源代码:

这里是 Ruby 源代码的链接:https://github.com/ruby/ruby/blob/trunk/numeric.c#L1587

Python 源代码从这里开始:http://hg.python.org/cpython/file/37352a3ccd54/Python/bltinmodule.c 并在这里完成:http://hg.python.org/cpython/file/37352a3ccd54/Objects/floatobject.c#l1080

后者有一个广泛的注释,揭示了两种实现之间的差异:

基本思想很简单:将双精度数转换并舍入为 使用 _Py_dg_dtoa 的十进制字符串,然后转换该十进制字符串 使用 _Py_dg_strtod 回到双倍。有一个小困难: Python 2.x 期望 round 从零开始进行半舍入,而 _Py_dg_dtoa 进行四舍五入。所以我们需要一些方法来检测和纠正中途的情况。

检测:中间值的形式为 k * 0.5 * 10**-ndigits 某个奇数k。或者换句话说,一个有理数 x 正好是 如果它的 2 值是 10**-ndigits 的两个倍数之间的一半 正好 -ndigits-1 并且它的 5-value 至少是 -n数字。对于 ndigits >= 0,二进制浮点 x 自动满足后一个条件,因为任何这样的浮点都具有非负数 5-估值。对于 0 > ndigits >= -22,x 需要是整数 5**-ndigits 的倍数;我们可以使用 fmod 进行检查。对于 -22 > ndigits,没有半途而废的情况:5**23 需要54位来表示 确切地说,因此对于 n >= 23,任何 0.5 * 10**n 的奇数倍数至少需要 精确表示的 54 位精度。

更正:处理中途案件的一个简单策略是 (仅适用于中途情况)使用参数调用 _Py_dg_dtoa ndigits+1 而不是 ndigits (因此可以精确转换为 十进制),手动舍入结果字符串,然后转换回来 使用 _Py_dg_strtod。

简而言之,Python 2.7 竭尽全力准确地遵循round-away-from-zero 规则。

在 Python 3.3 中,准确地遵循 round-to-even 规则也同样费力。

这里有一些关于_Py_dg_dtoa 函数的额外细节。 Python 调用 float to string 函数,因为它实现了一种算法,该算法在相等的替代方案中给出了最短的字符串表示形式。例如,在 Python 2.6 中,数字 1.1 显示为 1.1000000000000001,但在 Python 2.7 及更高版本中,它只是 1.1。 David Gay's sophisticated dtoa.c algorithm 给出了“人们期望的结果”而不放弃准确性。

该字符串转换算法往往会弥补一些困扰二进制浮点数上的任何 round() 实现的问题(即,它较少舍入 4.35 以 4.35 开头而不是 4.34999999999999996447286321199499070644378662109375)。

这和舍入模式(round-half-even vs round-away-from-zero)是 Python 和 Ruby round() 函数之间的本质区别。

【讨论】:

  • 比我如何回答这个问题
  • -1。虽然您引用了相关的源材料,但我不得不说您应该提取概念上的差异并实际解释它。我怀疑这是否对 OP 有帮助,甚至不清楚您自己是否理解它。我相信你确实明白,但是,为什么不直接解释一下呢?
  • @DigitalRoss -1 的答案是完全错误的。不应该将反对票用于您出于某种原因不喜欢的答案。只是不要赞成这样的答案。
  • 向下投票箭头的工具提示说“这个答案没用”。它没有说什么是对还是错。如果你问我“你能告诉我现在几点”,我说“是”,那么这个答案完全没有用,但仍然 100% 正确。
  • 恐怕这个答案中对 Python round 的描述不准确。 Python 的round 使用 Gay 的“最短字符串”代码,在任何时候都不会精确转换为字符串,也不会尝试撤消浮点表示错误。在round 源中,_Py_dg_dtoamode=3 调用,它只计算点之后的ndigits 正确舍入的数字(如果ndigits 为负数,则在该点之前)。相比之下,float.__repr__ 使用的最短字符串算法(例如)使用mode=0 调用。
【解决方案2】:

根本区别在于:

Python: 转成十进制再四舍五入

Ruby:    四舍五入,然后转换为十进制

Ruby 是从原来的浮点位串四舍五入的,但是在用 10n 对其进行运算之后。你看不到原始二进制值,无需仔细查看。这些值是不精确的,因为它们是二进制的,而且我们习惯于用十进制编写,而且碰巧我们可能编写的几乎所有十进制小数字符串都没有与基数为 2 的小数字符串完全等价。

特别是 0.45 看起来像这样:

01111111101 1100110011001100110011001100110011001100110011001101 

十六进制,即3fdccccccccccccd.

它以二进制重复,第一个未表示的数字是0xc,,巧妙的十进制输入转换已将最后一个小数数字精确地四舍五入为0xd

这意味着在机器内部,该值大约比0.45 大1/250。这显然是一个非常非常小的数字,但足以导致默认的 round-nearest 算法向上舍入而不是 even 的决胜局。

Python 和 Ruby 都可能进行多次舍入,因为每个操作都会有效地舍入到最低有效位。

我不确定我是否同意 Ruby 做了人类会做的事情。我认为 Python 正在逼近 十进制算术会做的事情。 Python(取决于版本)将 round-nearest 应用于十进制字符串,而 Ruby 将 round-nearest 算法应用于计算的二进制值。

请注意,我们可以在这里很清楚地看到人们说 FP 不精确的原因。这是一个相当正确的陈述,但是 更正确 说我们根本无法在二进制和大多数小数之间进行准确转换。 (有些人会这样做:0.25、0.5、0.75,...)大多数简单的十进制数字都是二进制重复数字,因此我们永远无法存储精确的等效值。但是,我们可以存储的每个值都是精确已知的,并且对其执行的所有算术运算都是精确执行的。如果我们首先将分数写成二进制,我们的 FP 算术将被视为精确

【讨论】:

  • 换句话说,Python 实现round(f, n)s = '%.*f' % (n, f); return float(s[:s.index('.') + n + 1] 非常相似,只是对中途情况进行了特殊处理。 应该被描述为“人类会做什么”——令人着迷。
  • 但如果这是实现,为什么round(1.45, 1) 不作为1.5 出来?查看代码,它应该将 1.45 转换为"1.45"(两位小数:一位用于舍入,一位用于处理中途情况),通过将"1.45" 更改为"1.5" 来手动处理中途情况,并转换"1.5"到-1.5。但字符串输入为"1.5" 并转换为浮点打印为1.5,而不是1.4
  • 有更多的复杂性。 Python 的早期版本使用从零开始舍入的模式,它甚至不是 IEEE-754 的 五种 模式之一。以后的版本在 round-nearest 上使用了一个变体,它在 IEEE-754 中打破了偶数的联系。这会将 1.45 舍入到 1.4 如果小数转换没有低阶残差。
  • 我可以用 Python 2.7 重复 round(1.45) -> 1.4 implements round-away-from-zero。查看代码,我怀疑 halfway_case 检测对该数字的评估结果为 false,因此永远不会触发本来可以工作的中途检测。 IE。 _Py_dg_dtoa 正确处理所有内容,除了中途检测。
【解决方案3】:

Ruby 不会作弊。它只是选择了另一种方式来实现round

在 Ruby 中,9.45.round(1) 几乎等同于 (9.45*10.0).round / 10.0

irb(main):001:0> printf "%.20f", 9.45
9.44999999999999928946=> nil
irb(main):002:0> printf "%.20f", 9.45*10.0
94.50000000000000000000=> nil

所以

irb(main):003:0> puts 9.45.round(1)
9.5

如果我们在 Python 中使用这种方式,我们也会得到 9.5。

>>> round(9.45, 1)
9.4
>>> round(9.45*10)/10
9.5

【讨论】:

    猜你喜欢
    • 2012-09-09
    • 2019-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-04
    相关资源
    最近更新 更多