别用 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 文档。

posted @ 2026-11-01 10:56  蜘蛛人  阅读(8)  评论(0)    收藏  举报