递归遍历+路径累积可精准定位差异点,关键在于传入当前路径列表、按类型分治处理、为list中dict提供id字段映射对齐、浮点数启用tolerance与NaN特殊判断,路径统一用list便于后续消费。

用递归遍历 + 路径累积获取差异位置
直接递归比对是最可控的方式,关键在于每层调用时把当前路径(如 ["user", "profile", "age"])作为参数传下去。遇到类型不一致、键缺失或值不同就立即记录路径,不继续深入。这样能精准定位到第一个差异点,也避免构建完整 diff 结构的开销。
注意:路径用 list 而不是 string 累积,方便后续拼接或转 dict.get() 形式;递归出口要覆盖 None、基本类型(str/int/bool)、list、dict 四类常见情况。
- 遇到
dict类型才继续递归,其他类型直接比较 - 若 a 有 key 而 b 没有,记录路径 +
"missing_in_b" - 若 b 有 key 而 a 没有,记录路径 +
"missing_in_a" - 若值都是 dict 但内容不同,继续递归;否则视为
"value_mismatch"
处理 list 差异时不要默认按索引对齐
嵌套 dict 中常含 list(如 {"items": [{"id": 1}, {"id": 2}]}),若简单按索引比对,[{"id": 2}, {"id": 1}] 会被判为全量不一致。实际中更合理的做法是:仅当 list 元素是基本类型(str/int等)时才逐索引比;若元素是 dict,优先按某个唯一字段(如 "id")做映射匹配,再比对子项。
这需要提前约定“可识别字段”,例如传入 id_fields={"items": "id"}。没有约定时,退化为索引比对,但必须明确提示该行为。
- list 长度不同 → 记录
"list_length_mismatch"并终止该分支 - 元素为 dict 且指定了 id 字段 → 构建
{id_value: dict}映射后比对 - 元素为 dict 但未指定 id 字段 → 按索引比,路径末尾加
[0]、[1]等
避免因浮点精度或 NaN 导致误报
两个 dict 若含浮点数,3.14 == 3.1400000000000001 会返回 False,但业务上可能认为相等。同理,float("nan") == float("nan") 恒为 False,需单独判断。
建议提供 tolerance=1e-9 参数控制数值容差,并在进入比对前用 math.isclose() 替代 ==;对 NaN 统一用 math.isnan() 判断是否均为 NaN。
- 只对
float和int类型启用 tolerance 比较 - 先检查是否都为
NaN,是则视为相等 - 避免对字符串数字(如
"3.14")自动转 float 比较,保持原始类型语义
路径格式统一用 list 表示,支持后续消费
返回的差异路径必须是 list(如 ["data", "users", 0, "name"]),而不是拼好的字符串(如 "data.users[0].name")。前者可直接用于 functools.reduce(dict.get, path, target) 定位值,也便于前端渲染成树形结构或生成 patch 操作。
若需输出可读字符串,应由调用方决定格式(".".join(map(str, path)) 或带括号形式),底层函数不耦合展示逻辑。
- 字典 key 用原值(
"user-id"不转下划线) - list 索引用
int,不用str(0而非"0") - 路径为空 list 表示顶层差异(如类型完全不同)










