---
title: "金额差一分钱怎么查：Decimal 的输入与舍入时机"
date: 2026-11-12
slug: python-decimal-rounding
canonical: https://kernelranch.com/blog/2026/11/python-decimal-rounding
markdown_url: https://kernelranch.com/blog/2026/11/python-decimal-rounding.md
tags: ["python", "decimal", "data-quality"]
---金额对不上时，先记录原始十进制字符串、舍入规则和舍入发生的位置。使用 Decimal 后，逐行舍入再求和与求和后舍入仍可能产生不同结果，接口和账单必须选定同一套规则。

## 1. 三笔小数就能复现差额

保存为 decimal_lab.py，运行 python decimal_lab.py。这里演示非负金额，最多十二位整数和四位小数，舍入到两位小数采用 ROUND_HALF_UP。这是本例的业务约定。

```python
import re
from decimal import Decimal, ROUND_HALF_UP, localcontext

CENT = Decimal("0.01")

def parse_amount(text):
    if not isinstance(text, str) or not re.fullmatch(r"(0|[1-9][0-9]{0,11})(\.[0-9]{1,4})?", text):
        raise ValueError("invalid amount")
    return Decimal(text)

def settle(amount):
    return amount.quantize(CENT, rounding=ROUND_HALF_UP)

with localcontext() as ctx:
    ctx.prec = 32
    exact = parse_amount("2.675")
    assert settle(exact) == Decimal("2.68")
    assert settle(Decimal(2.675)) == Decimal("2.67")
    rows = [parse_amount("0.005")] * 3
    per_line = sum(map(settle, rows), Decimal("0"))
    on_total = settle(sum(rows, Decimal("0")))
    assert (per_line, on_total) == (Decimal("0.03"), Decimal("0.02"))
    print("from string:", settle(exact))
    print("from float:", settle(Decimal(2.675)))
    print("per line:", per_line, "on total:", on_total)

for invalid in ["NaN", "Infinity", "1e9", "-1", "1.23456"]:
    try:
        parse_amount(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError(invalid)
print("invalid inputs: rejected")
```

## 2. 把差额定位到具体步骤

| 实测结果 | 发生了什么 | 修复位置 |
|---|---|---|
| 字符串输入得到 2.68，float 输入得到 2.67 | float 在进入 Decimal 前已有二进制表示误差 | 从 JSON 字符串或原始十进制文本解析 |
| 逐行结算 0.03，整单结算 0.02 | 三次舍入与一次舍入产生不同累计值 | 明确行项目和总额的结算顺序 |
| NaN、指数形式或过多小数被拒绝 | 输入超出本例契约 | API 返回字段校验错误 |

从客户端到数据库都要保留金额的表示约定。若客户端先算成浮点数再转成字符串，服务端收到的误差已经存在。整数分适合已经结算到分的金额，折扣比例和中间计算仍需要独立精度。

## 3. 分摊折扣时验收总和

例如将 1.00 平均分给三行，先分配 0.33、0.33、0.33，还剩 0.01。按固定规则把余数给某一行，例如按行 ID 排序后分配。验收时同时检查每行精度、各行之和、负数或退款的规则。将分摊结果存下来，后续退款引用原结果。

示例上下文精度 32 足够覆盖这些小数据。批量汇总和乘除运算需要根据金额上限、数量和中间结果重新确定精度。新增舍入规则时，用半分边界和正负数各做一组回归用例。

## 小结

保留十进制输入，显式指定舍入规则，并在固定的业务步骤结算。对账时同时保留行金额和总金额，才能定位差额发生在哪一次计算。

## 数据来源与检索时间

检索时间 2026-11-12。

| 来源 | URL | 对应内容 |
|---|---|---|
| Python 3.11 官方文档 | https://docs.python.org/3.11/library/decimal.html | Decimal 构造、上下文精度与 quantize |

## 核验说明

- 主示例在本地 Python 3.11.9 下执行，检查正文中的断言与输出。
- 实验范围是文中构造的输入和本地运行条件，部署时需按实际数据规模与运行环境核验。
