为什么 Ruby 的 Float#round 行为与 Python 不同?

Why is Ruby's Float#round behavior different than Python's?

提问人:steenslag 提问时间:4/1/2013 最后编辑:Communitysteenslag 更新时间:1/19/2016 访问量:1114

问:

"Python 中“round”函数的行为“观察到 Python rounds 像这样浮动:

>>> 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
Python Ruby 舍入 浮点精度

评论

0赞 Karoly Horvath 4/1/2013
这确实很奇怪。printf "%.20f", 1.45 # => 1.44999999999999995559 1.45.round 1 # => 1.5
0赞 Colonel Panic 4/4/2013
你的意思是,为什么在 Python 中返回,但在 Ruby 中返回?round(1.45,1)1.41.45.round(1)1.5

答:

10赞 Raymond Hettinger 4/1/2013 #1

总结

这两种实现都面临着围绕二进制浮点数s 的相同问题。

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

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

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

此外,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

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

基本思想非常简单:将 double 转换并四舍五入为 十进制字符串_Py_dg_dtoa,然后转换该十进制字符串 回到_Py_dg_strtod的双打。有一个小困难: Python 2.x 期望 round 执行 round-half-away-from-0,而 _Py_dg_dtoa做四舍五入到偶数。因此,我们需要一些方法来检测和纠正中途情况。

检测:中途值的格式为 k * 0.5 * 10**-ndigits 一些奇数整数 k。或者换句话说,一个有理数 x 正好是 介于 10**-ndigits 的两个倍数之间,如果其 2 估值为 正好是 -ndigits-1,它的 5 估值至少是 -ndigits。对于 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(因此精确转换为 decimal),手动舍入生成的字符串,然后转换回 使用_Py_dg_strtod。

简而言之,Python 2.7 竭尽全力准确地遵循从零开始四舍五入的规则。

在 Python 3.3 中,精确遵循四舍五入规则的长度同样长。

以下是有关_Py_dg_dtoa功能的一些额外细节。Python 调用 float to string 函数,因为它实现了一种算法,该算法在相等的替代方案中提供尽可能短的字符串表示形式。例如,在 Python 2.6 中,数字 1.1 显示为 1.100000000000000001,但在 Python 2.7 及更高版本中,它只是 1.1。David Gay 复杂的 dtoa.c 算法在不牺牲准确性的情况下提供了“人们期望的结果”。

该字符串转换算法倾向于弥补困扰二进制浮点数上 round() 的任何实现的一些问题(即,4.35 的舍入较少,从 4.35 开始,而不是 4.349999999999999999447286321199499070644378662109375)。

这和舍入模式(舍入-半偶数与舍入-从零开始)是 Python 和 Ruby round() 函数之间的本质区别。

评论

2赞 Karoly Horvath 4/1/2013
打败了我如何回答这个问题
0赞 DigitalRoss 4/1/2013
-1.虽然你引用了相关的原始资料,但我不得不说你应该提取概念上的差异并实际解释它。我怀疑这是否对 OP 有帮助,甚至不清楚你自己是否理解它。我相信你确实明白了,但是,为什么不直接解释一下呢?
5赞 ovgolovin 4/1/2013
@DigitalRoss -1 表示彻头彻尾的错误答案。反对票不应该用于你出于某种原因不喜欢的答案。只是不要对这样的答案投赞成票。
3赞 Jörg W Mittag 4/1/2013
反对箭头的工具提示显示“此答案无用”。它没有说对错。如果你问我“你能告诉我现在几点吗”,我说“是的”,这个答案是完全没用的,但仍然是 100% 正确的。
1赞 Mark Dickinson 11/8/2016
恐怕这个答案中对 Python 的描述并不准确。Python 使用 Gay 的“最短字符串”代码,不会在任何时候精确转换为字符串,也不会尝试撤消浮点表示错误。在源中,用 调用,它只是计算点之后(如果点为负数,则在点之前)正确计算四舍五入的数字。相反,使用(例如)by 的最短字符串算法是使用 调用的。roundroundround_Py_dg_dtoamode=3ndigitsndigitsfloat.__repr__mode=0
8赞 DigitalRoss 4/1/2013 #2

根本区别在于:

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

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

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

具体而言,0.45 如下所示:

01111111101 1100110011001100110011001100110011001100110011001101 

在十六进制中,这是3fdccccccccccccd.

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

这意味着在机器内部,该值大约大于 1/250。这显然是一个非常非常小的数字,但它足以导致默认的四舍五入算法四舍五入,而不是偶数的决胜局。0.45

Python 和 Ruby 都可能四舍五入不止一次,因为每个操作都有效地四舍五入到最低有效位。

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

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

评论

0赞 user4815162342 4/1/2013
换句话说,Python 的实现方式与 一样,对中途情况进行了特殊处理。应该被描述为“人类会做什么”——令人着迷。round(f, n)s = '%.*f' % (n, f); return float(s[:s.index('.') + n + 1]
0赞 user4815162342 4/1/2013
但是,如果这是实现,为什么不出来呢?查看代码,它应该将 1.45 转换为(两个小数点:一个用于舍入,另一个用于处理中途情况),通过更改为 手动处理中途情况,然后转换为 - 。但是字符串输入为 并转换为浮点数打印为 ,而不是 !round(1.45, 1)1.5"1.45""1.45""1.5""1.5"1.5"1.5"1.51.4
1赞 DigitalRoss 4/1/2013
还有更多的复杂性。早期版本的 Python 使用从零开始舍入的模式,该模式甚至不是五种 IEEE-754 模式之一。后来的版本在round-nearest上使用变体,在IEEE-754中,它打破了与偶数的联系。如果十进制转换中没有低阶残差,这将四舍五入 1.45 到 1.4。
0赞 user4815162342 4/1/2013
我可以用 Python 2.7 重复一遍,它实现了从零开始的舍入。查看代码,我怀疑此数字的检测结果为 false,因此从未触发过本来有效的中途检测。也就是说,除了中途检测外,一切都得到了正确的处理。round(1.45) -> 1.4halfway_case_Py_dg_dtoa
3赞 nymk 4/1/2013 #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