Python 统计金额:Decimal 为什么应从字符串创建
写一个订单汇总脚本时,最容易被忽略的细节往往是金额类型。把价格转成 float 再相加,屏幕上看起来可能没问题,但内部数值未必等于输入的十进制小数。下面用 Python 标准库 decimal 演示一个小范围金额统计,并把输入约束和舍入规则写清楚。
显示两位小数,不等于计算过程精确
from decimal import Decimal
print(0.1 + 0.2) # 0.30000000000000004
print(Decimal("0.1") + Decimal("0.2")) # 0.3
print(Decimal(0.1) == Decimal("0.1")) # False
许多十进制小数不能被二进制浮点数精确表示。Decimal(0.1) 接收到的已经是浮点近似值,它不会自动还原用户原本输入的“0.1”。因此,金额来自 CSV、表单或配置文件时,尽量保留原始文本,校验后直接交给 Decimal。先转 float、再转字符串,会把早先计算产生的误差一起带过去。
格式化 f"{value:.2f}" 只改变输出形式,不能修复前面已经发生的计算偏差。
把输入规则和计算规则分开
这个示例接受可选负号、1到12位整数、最多两位小数;负数可用于演示退款。它拒绝空白、千位分隔符、科学计数法以及超过两位的小数。超出规则时应明确报错,不静默截断用户输入。
import re
from decimal import Decimal, localcontext, ROUND_HALF_UP
AMOUNT = re.compile(r"-?[0-9]{1,12}(?:\.[0-9]{1,2})?")
CENT = Decimal("0.01")
def parse_amount(text):
if not isinstance(text, str) or AMOUNT.fullmatch(text) is None:
raise ValueError("金额须为普通十进制文本,整数1到12位,小数最多2位")
return Decimal(text)
with localcontext() as ctx:
ctx.prec = 40
prices = [parse_amount(x) for x in ["19.90", "9.99", "0.10"]]
total = sum(prices, Decimal("0.00"))
fee = (total * Decimal("0.06")).quantize(CENT, rounding=ROUND_HALF_UP)
print(f"合计:{total:.2f},示例费用:{fee:.2f}")
# 合计:29.99,示例费用:1.80
fullmatch 要求整个输入符合规则,避免“12.30元”或末尾换行被误当成合法金额。正则也自然排除了 Decimal 支持的 NaN、Infinity 等特殊值。这里不自动 strip,是为了让数据格式问题尽早暴露;如果业务允许前后空格,应在入口明确规定清理策略。
localcontext 把本段运算的有效数字精度设为40,退出后恢复调用者环境。这足以覆盖本例的三笔受限金额;实际批量汇总仍应根据单笔上限、条数和运算方式设定精度,不能把 Decimal 理解成无限精度计算器。
舍入发生在哪里,要由业务定义
quantize(CENT) 把结果调整为两位小数,ROUND_HALF_UP 明确指定中点向远离零的方向舍入,例如2.345变成2.35,-2.345变成-2.35。它与默认常见的 ROUND_HALF_EVEN 不同,也不应与所有业务的“四舍五入”规则直接画等号。
示例采用“先汇总、再计算费用、最后舍入”。如果规则要求每一行先算费用再舍入,最终结果可能不同。两笔0.05分别按10%计算并各自舍入,会得到0.01加0.01;先合计0.10再按10%计算,则得到0.01。代码应准确表达约定,不能靠最后格式化掩盖差异。
输出到接口时,可以将金额编码为十进制字符串,例如"29.99",或在协议明确规定后使用整数分;不要为了JSON序列化方便又转回float。类型、允许的输入和舍入时点一起确定,金额统计才更容易复查。
本文由AI辅助整理,文中示例及非法输入、舍入边界已用Python标准库在本地验证;6%的费用仅为计算演示,不代表实际税率或收费规则。
- 点赞
- 收藏
- 关注作者
评论(0)