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

时间:2013-03-31 22:45:56

标签: python ruby rounding floating-accuracy

" 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

3 个答案:

答案 0 :(得分:10)

<强>摘要

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

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

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

使用最短的字符串表示,Python使用精确的字符串操作舍入到适当的小数位数。浮点到字符串转换的目标是尝试“撤消”一些二进制浮点表示错误(即如果输入6.6,则在6.6上的Python轮次而不是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返回到double。有一个小难点:   Python 2.x希望round能够从零开始一半,而且   _Py_dg_dtoa做了一半到一半。所以我们需要一些方法来检测和纠正中途病例。

     

检测:中间值的形式为k * 0.5 * 10 ** - ndigits for   一些奇数k。换句话说,有理数x就是   如果它的2估值是,则在两倍于10 **的两倍之间 - ndigits   正是-ndigits-1及其5估值至少是   -ndigits。对于ndigits&gt; = 0,后一个条件会自动满足二元float x,因为任何这样的float都具有非负的   5估值。对于0> ndigits&gt; = -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调用为字符串函数,因为它实现了一种算法,该算法在相同的替代项中提供尽可能短的字符串表示。例如,在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.3499999999999996447286321199499070644378662109375进行四舍五入。)

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

答案 1 :(得分:7)

根本区别在于:

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

Ruby: 回合然后转换为十进制

Ruby将其从原始浮点位字符串舍入,但在使用 10 n 进行操作后。您看不到原始二进制值没有仔细观察。这些值是不精确的,因为它们是二进制的,我们习惯用十进制写,因为几乎所有我们可能写的小数部分字符串都没有作为基数2分数字符串的精确等价。

特别是,0.45看起来像这样:

01111111101 1100110011001100110011001100110011001100110011001101 

以十六进制表示,即3fdccccccccccccd.

它以二进制形式重复,第一个未表示的数字是0xc,,聪明的十进制输入转换已经精确地将最后一个小数位舍入为0xd

这意味着在机器内部,该值大于0.45大约1/2 50 。这显然是一个非常非常小的数字,但它足以导致默认的舍入最近的算法向上舍入而不是甚至的决胜局。

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

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

请注意,我们可以清楚地看到人们说FP不精确的原因。这是一个相当真实的陈述,但是说更真实表示我们根本无法在二进制和大多数小数部分之间准确转换。 (有些做:0.25,0.5,0.75,...)大多数简单的十进制数是二进制的重复数,所以我们永远不能存储完全等价的值。但是,我们可以存储的每个值都是准确知道的,并且对它执行的所有算术都是精确执行的。如果我们首先用二进制编写分数,我们的FP算法将被视为 exact

答案 2 :(得分: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