如无特殊说明,本文中的 Python 均指代 Python 3

Citation:

  1. https://www.v2ex.com/t/550492
  2. https://docs.python.org/3.6/library/functions.html#round
  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round

现在(2019年)最火的语言是 Python,发展最快、潜力最大的语言是 JavaScript。这俩语言有很多类似的地方,市面上也有一些类似于 gh: PythonJS/PythonJSgh: PiotrDabkowski/Js2Py 等的 JS 代码和 Python 代码互转的库。

但是呢,仔细揪一揪这两个语言的不同之处,其实还是大有文章可书的:比如今天要说的 round()Math.round()

Test Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
round(1.5)      # 2
round(1.4) # 1
round(3.0) # 3
round(-1.6) # -2
round(-1.4) # -1
round(1.13, 1) # 1.1
round(1.15, 1) # 1.1
round(1.35, 1) # 1.4
round(1.25, 1) # 1.2
round(True) # 1
round(False) # 0
round("100.2") # TypeError: type str doesn't define __round__ method
round("1e3") # TypeError: type str doesn't define __round__ method
round("100px") # TypeError: type str doesn't define __round__ method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Math.round(1.5)      // 2
Math.round(1.4) // 1
Math.round(3.0) // 3
Math.round(-1.6) // -2
Math.round(-1.4) // -1
Math.round(1.13, 1) // 1
Math.round(1.15, 1) // 1
Math.round(1.35, 1) // 1
Math.round(1.25, 1) // 1
Math.round(true) // 1
Math.round(false) // 0
Math.round("100.2") // 100
Math.round("1e3") // 1000
Math.round("100px") // NaN

具体分析

观察一下大概可以发现似乎两个语言的 round 函数的单参数用法都是四舍五入到整数上。

坑 0:Python 2 和 Python 3 中的 round 行为不一致

参见 Citation[2] 和 https://docs.python.org/2.7/library/functions.html#round

Python 2 中的 round 采用四舍五入,而 Pyhton 3 中的 round 采用的是大学物理使用的四舍六入五成双(后文会详细解释)。

坑 1:round “五入”朝向 0 还是朝向更大的方向

参见 Test Case 4 & 5

Python 把 -1.6-1.4 round 到 -2-1,而 JS 则将二者 round 到 -1-2。这是因为 Python 会往绝对值更大的方向“五入”、绝对值更小的方向“四舍”,而 JS 则是往值更大的方向“五入”、值更小的方向“四舍”。

坑 2:round 的参数个数

参见 Test Case 6-9

Python 的 round 接受 1 至 2 个参数,而 JavaScript 的 Math.round 只接受一个参数。所以 Python 可以指定舍入精度,而 JS 则只能舍入到整数。

坑 3:浮点数精度

参见 Test Case 7

在 Python 中,1.15 被舍入为 1.1,这是因为 1.15 实际被存储为 1.149999999999999911...,该数舍入到小数点后 2 位时符合“四舍”规则。同样,0.645 被存储为 0.645000000000000017...,舍入到小数点后 2 位时符合五入规则。

使用 JS 的 Number.prototype.toFixed 函数可以看到更多位:

1.15.toFixed(100) 得到了

1.1499999999999999111821580299874767661094665527343750000000000000000000000000000000000000000000000000

坑 4:类型转换

参见 Test Case 10-14

JS 和 Python 的 round 都可以正确处理布尔值。然而其内在实现机理是不同的:

  • JS 尝试使用 Number 函数将输入转为数字,再对其进行常规的 round 操作。
  • Python 尝试调用输入的值的 __round__ 方法,并返回其结果。由于字符串(str 类)没有定义 __round__ 方法,所以无法被 round 函数处理。

坑 5:“四舍六入五成双”

参见 Test Case 9

补充阅读:使用O(1)时间复杂度计算比x大的最小的2的整数次幂 中的浮点数相关内容

“坑 3” 提到的浮点数精度丢失并不能解释 Test Case 9 的结果,因为 Python 和 JS 在底层都使用 IEEE 754 双精度浮点数来表示小数,二者在小数表示上精度相同,但是 Test Case 9 的 Python 和 JavaScript 结果不同。

查阅资料,发现 Python 的 round 遵循的是“四舍六入五成双”的舍入规则,即:

*当要保留__小数点后 \(n\) 位__时,*

  • 如果第 \(n+1\) 位不为 5,按照四舍五入规则舍入。
  • 如果第 \(n+1\) 位为 5,但是其后仍有有效数字,则进位。
  • 如果第 \(n+1\) 位为 5,且其后没有任何有效数字:
    • \(n\) 位为奇数时,进位;
    • \(n\) 位为偶数时,舍去。

该舍入规则使科学计算中舍入时累积误差更小。

1.25 保留小数点后一位时,小数点后第三位为 0,第二位为 5,第一位为偶数,所以舍去第二位的 5,第一位保持不变。

这也意味着 Python 中 round(2.5) 为 2,想必困扰了无数初学者 233333

按照这个规则,round(2.675, 2) 应该得到 2.68,但是实际得到了 2.67。这是因为 2.675 踩到了坑 3,其底层表示为 2.6749999999...

JavaScript 的 round 则是简单的四舍五入,没有这么多门道。

备注:如果想在 Python 里实现四舍五入,可以使用 decimal 库:

1
2
3
4
5
6
7
import decimal

# 修改舍入方式为四舍五入
decimal.getcontext().rounding = "ROUND_HALF_UP"

# 使用字符串来储存小数不会有精度误差,Decimal可以正确处理这种方法表示的数字
decimal.Decimal("2.33333").quantize(decimal.Decimal("0.00"))

来源:https://blog.jiejiss.com/