Python Decimal 舍入:把精度、位数和规则分开
报表导出或测量结果保留两位小数,看起来只需要一个格式化操作。真正容易出错的是:输入是否已损失精度,中间计算保留多少有效数字,最终采用什么舍入规则。本文以 Python 3.14 官方文档核验语义,全部代码实际运行环境为 CPython 3.12.14;没有把文档版本写成实测版本。
AI生成概念配图:十进制数字分别经过输入、计算精度与最终舍入三道检查
从文本进入十进制运算
若原始数据以十进制文本提供,优先直接传给 Decimal。先转 float 再转回来,会把二进制浮点数已有的近似值带进来。Decimal(0.1) 与 Decimal("0.1") 并不相等;把一个已经算错或舍入过的浮点结果转成字符串,也不能恢复丢失的信息。输入链路比最后的显示格式更早决定结果。
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
unit = Decimal('0.01')
for text in ('1.245', '1.255', '-1.245'):
value = Decimal(text)
print(text, value.quantize(unit, rounding=ROUND_HALF_EVEN),
value.quantize(unit, rounding=ROUND_HALF_UP))
print(Decimal(0.1) == Decimal('0.1'))前三行输出分别为 1.245 1.24 1.25、1.255 1.26 1.26、-1.245 -1.24 -1.25,最后输出 False。两种模式都先取最近值,恰在中点时才分歧:HALF_EVEN 朝偶数末位,HALF_UP 远离零。因此不能把后者概括为“遇到五就向正方向加一”,负数就是最直接的反例。
有效数字不等于小数位数
上下文 prec 约束运算结果的有效数字数量;quantize 的模板约束结果指数。模板 0.01 表示保留到百分位,不是把所有运算精度改为两位。字符串构造本身也不会按 prec 截短输入。若结果所需有效数字超过上下文精度,quantize 可能触发 InvalidOperation,而不是自动提高精度。
from decimal import localcontext, InvalidOperation
with localcontext() as ctx:
ctx.prec = 3
try:
Decimal('1234').quantize(Decimal('0.01'))
except InvalidOperation:
print('precision too small')
print(Decimal('1.025').quantize(
Decimal('0.05'), rounding=ROUND_HALF_UP))上段输出 precision too small 和 1.03。第二个结果尤其容易误解:quantize 读取的是模板指数,所以 0.05 仍然只指定两位小数,并不要求结果为五百分之一的整数倍。若业务真的要求按固定步长取整,需要先除以步长、按指定规则取整数,再乘回,并额外评估除法精度与边界测试。
校验和舍入应由不同规则驱动
from decimal import Inexact
with localcontext() as ctx:
ctx.traps[Inexact] = True
try:
Decimal('1.234').quantize(Decimal('0.01'))
except Inexact:
print('extra nonzero decimal places')该段打印 extra nonzero decimal places,适合“超出两位且会改变数值就拒绝”的输入要求。它不等价于检查原始字符串长度:1.230 去掉尾零不会造成数值不精确,因此不能靠这个陷阱拒绝所有第三位字符。文本格式限制与数值精度限制应分别测试,特殊值、输入长度及允许范围也要显式约束。
用汇总测试确定舍入位置
再考虑两次测量各得到 0.004。若每项先保留两位再相加,总值是 0.00;若先相加再保留两位,则得到 0.01。二者差异来自流程,而不是库偶尔不稳定。测试用例应同时写明原始输入、计算步骤和预期文本,避免重构时把舍入函数从输出层挪进循环,悄悄改变统计结果。
values = [Decimal('0.004'), Decimal('0.004')]
unit = Decimal('0.01')
print(sum(x.quantize(unit, rounding=ROUND_HALF_EVEN) for x in values))
print(sum(values).quantize(unit, rounding=ROUND_HALF_EVEN))上段已执行,依次打印 0.00 与 0.01。项目应该固定采用哪条路径,并记录规则由哪个需求决定。修改规则时加入回归样本,重新核对汇总及导出结果;不要只看页面展示相同,就认为内部数值也相同。保留尾零有时也是输出协议的一部分,应与数值比较分开验收。
尽量只在约定边界舍入一次
value = Decimal('1.2451')
unit = Decimal('0.01')
print(value.quantize(unit, rounding=ROUND_HALF_EVEN))
print(value.quantize(Decimal('0.001'), rounding=ROUND_HALF_EVEN)
.quantize(unit, rounding=ROUND_HALF_EVEN))输出分别是 1.25 与 1.24。第一次保留三位产生了新的中点,随后再保留两位就改变答案。可以为计算保留经过评估的额外精度,在交付边界统一 quantize,并用 localcontext 限定设置影响范围。但额外精度不是无限精确的承诺;长运算链仍要分析误差。把规则写成具名函数并测试正负临界值,通常比在多处复制格式化表达式更容易维护。


