13

Python 中“round”函数的行为”观察到 Python 的 round 浮动如下:

>>> 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 怎么会像人类一样浮动呢?鲁比会作弊吗?

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
4

3 回答 3

10

概括

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

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

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

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

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

细节

鲁比不作弊。它以与 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/浮动对象.c#l1080

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

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

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

更正:处理中途情况的一个简单策略是(仅针对中途情况)使用参数 ndigits+1 而不是 ndigits 调用 _Py_dg_dtoa(从而精确转换为十进制),手动舍入结果字符串,然后转换使用 _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.349999999999999964472863211199499070644378662109375 开头)。

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

于 2013-03-31T22:54:44.723 回答
8

根本区别在于:

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

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

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

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

01111111101 1100110011001100110011001100110011001100110011001101 

在十六进制中,即3fdccccccccccccd.

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

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

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

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

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

于 2013-03-31T23:45:34.190 回答
3

鲁比不作弊。它只是选择了另一种实现方式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
于 2013-04-01T00:02:13.707 回答