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

ste*_*lag 13 ruby python rounding floating-accuracy

" Python中"round"函数的行为 "观察到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
Run Code Online (Sandbox Code Playgroud)

接受的答案证实这是由于浮点数的二进制表示不准确引起的,这都是合乎逻辑的.

假设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
Run Code Online (Sandbox Code Playgroud)

Ray*_*ger 10

摘要

这两种实现都面临着围绕二进制浮点数 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')
Run Code Online (Sandbox Code Playgroud)

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

这是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将double转换并舍入为十进制字符串,然后使用_Py_dg_strtod将该十进制字符串转换回double.有一个小难度:Python 2.x期望舍入为零,半径为零,而_Py_dg_dtoa为舍入为半舍入到偶数.所以我们需要一些方法来检测和纠正中途病例.

检测:中间值的形式为k*0.5*10** - 对于某个奇数k,ndigits.或者换句话说,如果其2估值恰好是-ndigits-1且其5估值至少为-ndigits,则有理数x正好在两个10** - ndigits的倍数之间.对于ndigits> = 0,后者条件会自动满足二元float x,因为任何此类float都具有非负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竭尽全力准确地遵循从零开始的规则.

在Python 3.3中,准确遵循舍入到偶数规则的长度同样很长.

这是_Py_dg_dtoa函数的一些额外细节.Python将float调用为字符串函数,因为它实现了一种算法,该算法在相同的替代项中提供尽可能短的字符串表示.例如,在Python 2.6中,数字1.1显示为1.1000000000000001,但在Python 2.7及更高版本中,它只是1.1.David Gay先进的dtoa.c算法在不放弃准确性的情况下给出了"人们期望的结果".

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

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

  • @DigitalRoss -1是完全错误的答案.Downvotes不应该用于您出于某种原因不喜欢的答案.只是不要赞成这样的答案. (5认同)
  • downvote箭头的工具提示说"这个答案没用".它没有说对或错.如果你问我"你能告诉我现在几点钟"并且我说"是",那么这个答案完全没用,但仍然100%正确. (3认同)
  • 打败了我如何回答这个问题 (2认同)

Dig*_*oss 8

根本区别在于:

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

Ruby:    Round然后转换为decimal

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

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

01111111101 1100110011001100110011001100110011001100110011001101 
Run Code Online (Sandbox Code Playgroud)

在十六进制中,即 3fdccccccccccccd.

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

这意味着在机器内部,该值大于0.451/2 50.这显然是一个非常非常小的数字,但它足以导致默认的最近舍入算法向上舍入而不是为偶数的平局.

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

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

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