用 Python Decimal 汇总 CSV 金额:避免浮点误差的入门示例
金额报表里,最难排查的问题往往不是程序报错,而是结果只差几分钱。本文用 Python 标准库读取 CSV,演示怎样保留金额精度,并把坏数据明确暴露出来。示例只在本地读文件,不依赖云资源或付费服务。
一、先定义输入约定
假设 orders.csv 使用 UTF-8 编码,第一行是表头,至少包含 amount 列。每一行金额采用十进制字符串,例如 19.90、0.10 或 -5.00;负数代表退款。空值不默认为零,因为缺失金额与真实的零是两种不同业务含义。示例也不接受千分位逗号或货币符号,避免把格式问题悄悄掩盖。
二、为什么不用 float
二进制浮点数不能精确表示所有十进制小数。例如,0.1 与 0.2 的浮点相加结果,直接显示时可能带有额外尾数。报表使用 Decimal 可以按十进制处理数值。关键是直接传入原始字符串:Decimal('0.10');不要先转成 float 再构造 Decimal,否则此前产生的近似值也会被带入。
三、可以直接运行的代码
from csv import DictReader
from decimal import Decimal, InvalidOperation
def total_amount(path):
total = Decimal('0')
with open(path, encoding='utf-8-sig', newline='') as f:
reader = DictReader(f)
if not reader.fieldnames or 'amount' not in reader.fieldnames:
raise ValueError('缺少 amount 表头')
for record_no, row in enumerate(reader, start=1):
raw = (row.get('amount') or '').strip()
try:
amount = Decimal(raw)
except InvalidOperation as exc:
raise ValueError(f'第 {record_no} 条记录金额非法:{raw!r}') from exc
if not amount.is_finite():
raise ValueError(f'第 {record_no} 条记录金额必须为有限数')
total += amount
return total
print(total_amount('orders.csv'))
utf-8-sig 可以兼容常见的 UTF-8 BOM 文件;newline='' 让 csv 模块处理换行。这里报告的是数据记录序号,不是文件物理行号,因为被引号包围的 CSV 字段可能跨行。is_finite() 用来拒绝 NaN 和 Infinity,避免这类特殊值污染汇总。
四、用最小样本核对结果
把下面三笔写入 amount 列:0.10、0.20、-0.05。预期总额是 0.25。再分别测试空金额、abc 和 NaN,应得到明确错误,而不是一个看似正常的总数。遇到编码错误时,应先确认文件编码,再选择正确的 encoding;不要用忽略错误的方式跳过字符。
五、把精度规则放进业务约定
Decimal 仍受当前上下文精度限制,并不等于无限精度。一般短金额示例可用默认上下文;超长数值和大规模累计应依据最大位数评估精度。是否按两位小数四舍五入、采用哪种舍入方式、逐笔舍入还是汇总后舍入,都需要由业务规则决定。不要为了让数字“好看”就随意 round。
这个示例的重点是三个边界:金额从字符串进入、异常数据及时停止、舍入策略显式约定。先把这三件事做好,再扩展分组汇总、日志和报表导出,会更容易核对结果。
- 点赞
- 收藏
- 关注作者
评论(0)