别用 float 记 credits:用 Decimal 做 SerpBase 对账
差的那一点点
月度对账,余额差值和请求扣费之和差了 0.0000001。肉眼看不出来,但脚本里的 if expected == actual 直接判定不平,半夜告警。
原因很朴素:credits_charged 是 JSON 里的数字,json.loads 默认把它解析成 float,几百次累加之后,二进制浮点的误差就攒出来了。接口返回的数值本身是准确的,问题出在客户端用 float 累加。
金额、额度、成本,只要涉及「求和之后要和另一个数相等」,就不该用 float。
从解析这一步就换成 Decimal
json.loads 支持 parse_float 钩子:
import json
from decimal import Decimal
data = json.loads(raw, parse_float=Decimal)
credits = data["credits_charged"]
这样从源头就是 Decimal,后面怎么加都不会漂。
如果用 orjson,它不直接支持 Decimal,需要自己转:
import orjson
from decimal import Decimal
data = orjson.loads(raw)
if isinstance(data.get("credits_charged"), float):
data["credits_charged"] = Decimal(str(data["credits_charged"]))
先 str() 再 Decimal(),别直接 Decimal(float)——后者会把 float 的误差原样带进来。
求和与比较
from decimal import Decimal
expected = sum((Decimal(str(x)) for x in charged_list), Decimal("0"))
actual = before["credits"] - after["credits"]
if expected != actual:
print("差异:", actual - expected)
sum 的初始值一定要给 Decimal("0"),给 0 会把结果拽回 int 参与运算,虽然这里结果碰巧没错,但类型链条一断,后面就容易混进 float。
落库用 NUMERIC
CREATE TABLE request_ledger (
request_id TEXT PRIMARY KEY,
credits NUMERIC(12, 4) NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL
);
NUMERIC 是精确类型,读出来再转 Decimal。别用 FLOAT/DOUBLE,数据库层的误差比 Python 更隐蔽——写进去的时候看不出,聚合几百行之后才开始偏。
四舍五入要显式
from decimal import Decimal, ROUND_HALF_UP
per_thousand = (total_usd / (credits / 1000)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
quantize 显式指定小数位和舍入模式,报表数字才不会一会儿 0.30、一会儿 0.29999999。银行家舍入还是四舍五入,写出来,别让默认行为替你决定。
我做错的一版
第一版用 float 存了一整月的扣费,对账脚本判定不平,我把差额打印出来是 1e-07,以为是查询漏了某条请求,排查了两个小时。
后来换成 Decimal,差额精确到 0,一次对平。不是数据错了,是表示方式错了。
现在扣费字段一律 Decimal,JSON 解析带 parse_float,数据库列 NUMERIC,报表 quantize。一次网页搜索 1 credit,新账号 500 免费 credits,账目小,但「对平」这件事不能靠运气。
SerpBase 的字段结构见 SerpBase 文档。

浙公网安备 33010602011771号