总结
两种实现都面临相同的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() 函数之间的本质区别。