round 不是四舍五入:2.5 舍成 2,负数区间也照旧

举报
yd_232353574 发表于 2026/11/08 09:27:50 2026/11/08
【摘要】 实测 Python 的 round 走的是银行家舍入,2.5 舍成 2、4.5 舍成 4,结果一律停在偶数上。负数同规则:-0.5 得 0、-1.5 得 -2;并纠正一个流传较广的说法,Python 2 的 round 是平局远离零,两个版本真正分歧的是负半值里的 -0.5、-2.5、-4.5。另用十个半值实测偏差抵消,并说明该抵消依赖分布对称。

对账时冒出来的怪数字

给人查一段金额处理,对方说 2.5 舍完竟然是 2,怀疑程序写错了。我起初也以为是浮点尾数在捣乱,可把值打印出来,干干净净一个 2.5。顺着查下去才明白,Python 的 round 压根没走小学那套满五进一。这类问题藏得深,多数时候结果看着都正常,只有正好落在 .5 上才会露出来。

把几个 .5 挨个过一遍

在本机 Python 3.13.14 上,把 0.5 到 5.5 全部测一遍:

for x in [0.5, 1.5, 2.5, 3.5, 4.5, 5.5]:
    print(x, "->", round(x))

输出:

0.5 -> 0
1.5 -> 2
2.5 -> 2
3.5 -> 4
4.5 -> 4
5.5 -> 6

规律一眼能看出来:结果全往偶数上靠。0.5、2.5、4.5 往下压,1.5、3.5、5.5 往上抬,共同点是结果一定是个偶数。

这套规则叫银行家舍入

它的正式名字是银行家舍入,也叫四舍六入五成双。碰到正好卡在两数中间的 .5,就看前一位是奇数还是偶数:是偶数就压下去,是奇数就抬上来,最终停在偶数。Python 从 3 开始就固定采用这套策略,并且把它写进了语言行为里,不是某个版本的临时实现。

负数半值也是同一套规则

负数这边常被当成例外,其实单参数形式下规则一模一样。下面只测负半值,同一套测法换成负数:

for x in [-0.5, -1.5, -2.5, -3.5, -4.5, -5.5]:
    print(x, "->", round(x))

输出:

-0.5 -> 0
-1.5 -> -2
-2.5 -> -2
-3.5 -> -4
-4.5 -> -4
-5.5 -> -6

镜像关系很整齐:正数那边 0.5 舍成 0,负数这边 -0.5 也舍成 0;正数 1.5 升到 2,负数 -1.5 降到 -2。在单参数形式下,这几个值都满足 round(-x) == -round(x),可以用下面这一行验证:

print(all(round(-x) == -round(x) for x in [0.5, 1.5, 2.5, 3.5, 4.5, 5.5]))

输出:

True

这里要留意网上流传较广的一种说法:负数一律往远离零的方向舍,round(-0.5) 得 -1。平局时远离零确实是 Python 2 的行为,那时 round(-0.5) 等于 -1.0;Python 3 改成平局取偶之后,同一个式子变成 0。但「负数一律得 -1」这个概括跟 Python 2 也对不上,因为 round(-1.5) 在两个版本里都是 -2,更像是把负数进位想当然地套了过来。在刚才测的这六个负半值里,两个版本之间变了的是 -0.5、-2.5、-4.5 这三处,正好是前一位为偶数的那些,Python 2 分别给 -1.0、-3.0、-5.0,Python 3 给 0、-2、-4;前一位为奇数的 -1.5、-3.5、-5.5 两边一致,因为远离零和取偶在奇数位上撞到同一个答案。

真正让人困惑的是浮点表示

再看两个数,容易觉得规则又变了:

print(round(2.500000000000001))
print(round(0.05, 1))

输出:

3
0.1

第一个得 3 好理解,2.500000000000001 比 2.5 大了一丁点,不算正中间,按更大处理直接进位。第二个 0.05 保留一位得到 0.1,看着又像满五进一了。其实 0.05 在二进制里存的值比十进制的 0.05 略大那么一点,它并没有真的落在中间,于是按略微更大往上走。用 Decimal 把它打出来,差值看得见:

from decimal import Decimal
print(Decimal(0.05) - Decimal("0.05"))

输出:

2.775557561562891351059079170E-18

差是正的,说明实际存的比字面写的略大。Decimal 默认精度 28 位,改了 getcontext().prec 输出会跟着变,但正负号不会变。两处都不是规则改了,是二进制浮点的表示误差在底下起作用。

想确认某个 float 是不是精确等于某个十进制字面量,直接判等更直观:

from decimal import Decimal
print(Decimal(0.5) == Decimal("0.5"))
print(Decimal(0.05) == Decimal("0.05"))

输出:

True
False

0.5 是二进制能精确表示的,所以它跟字面量相等;0.05 不行,存下来的值比字面写的略大,连跟字面量都不相等。判等回答的是「存下来的值是否精确等于这个十进制字面」,不是通用的判平局方法。

对称分布下偏差能互相抵消

这套规则常被说成偏差能抵消,拿十个 .5 就能验证:

vals = [i + 0.5 for i in range(10)]
print(sum(vals))
print(sum(round(v) for v in vals))

输出:

50.0
50

原始总和是 50.0,逐个舍入之后再加还是 50,一点没偏。要是换成满五进一,这十个值会全部往上抬,总和变成 55,凭空多出 5 个。这就是偏差抵消的实感:五次压下去,五次抬上来,账是平的。

不过抵消依赖样本分布对称。十个数里 .5 落在偶位和奇位各一半,账才刚好平;换成五个递增的半值,和是 12.5,舍入后 12,差 -0.5;换成 101 个递增的半值,和是 5100.5,舍入后 5100,仍只差 -0.5。可要是 101 个值全是同一个 0.5,原始和 50.5,逐个舍入后全成 0,账直接塌了。所以偏差抵消的前提是分布对称,一旦所有半值压在同一侧,偏差会原样累积。反复舍入的统计、财务场景,这个差别会被放大。

要严格满五进一就换 decimal

发票、报价这类按业务规矩必须满五进一的场合,别指望 round,把口径显式写出来才稳:

from decimal import Decimal, ROUND_HALF_UP
one = Decimal("1")
print(Decimal("2.5").quantize(one, rounding=ROUND_HALF_UP))
print(Decimal("3.5").quantize(one, rounding=ROUND_HALF_UP))

输出:

3
4

给 Decimal 传字符串这一步很要紧,先转成 float 就又会把表示误差带进来。上面两个恰好是二进制能精确表示的值,所以看不出差别;换成 0.05 就不行了,Decimal(0.05) 存的是那个偏大的近似值,只有 Decimal("0.05") 才是业务想要的十进制意图。

有个坑要单独提:ROUND_HALF_UP 的官方定义是平局时远离零,负数上也不例外,Decimal("-2.5") 在这个模式下得 -3,不是 -2。业务要负数也往大的方向进,可以换成 ROUND_CEILING,但它对所有值都朝正无穷取整,2.1 这样不是平局的数也会变成 3,并不是「满五进一的负数版」,用之前先确认这符合业务口径。

遇到 .5 先别急着改代码

我的排查顺序是两步:先把原始值转成 Decimal、跟十进制字面量比一下是否精确相等,确认它是不是真的恰好在一半,多数怪现象到这一步就解释清楚;再回头问清楚业务到底要哪种舍入口径,是银行家舍入还是满五进一。口径定错了,比写错一行代码藏得深得多。这次最大的收获不是记住 round 怎么算,而是意识到舍入底下叠着两套逻辑,一套是规则,一套是浮点表示,分开看才不会互相背锅。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。