登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python json.loads 怎样处理重复键

来源:17golang原创

时间:2026-11-03 12:20:31 424浏览 收藏

Python 的 json.loads 默认允许同一对象中出现重复键,只保留最后一次出现的值。想把重复键当作输入错误,需要设置 object_pairs_hook,在有序键值对列表被合并成字典前检查成员名。

我在选择配置读取方案时,更在意一个字段究竟由谁决定:如果业务约定只允许一个值,就应该让生产配置的人看到冲突。下面以一段虚构的任务队列配置说明这种取舍,代码和结果描述均用于演示。

Python 官方文档:https://docs.python.org/3/library/json.html

先明确这次迁移改变的是接收规则

这里的“迁移”指把应用原来的宽松读取改为拒绝重复成员,不是宣称升级 Python 后默认规则改变。Python 文档对默认保留最后值和自定义钩子都有明确说明。JSON 标准建议对象成员名唯一,同时指出不同实现面对重复名称时可能采用不同策略。跨系统传递配置时,不能只凭一个解析器的结果推断所有接收方的理解。

我会先把入口分成两类:自己控制生成器的配置,直接修生产端;必须兼容外部历史文件的入口,先约定切换日期和错误处理。不要把拒绝重复键的变更偷偷塞进一次运行时升级,让调用者误以为以前能导入的内容仍然全部可用。

把默认覆盖与显式拒绝放在一起比较

接收策略重复键的处理适合的接口约定
默认解析同一键保留最后值已经明确接受覆盖的输入
拒绝重复键钩子抛出异常每个成员只能出现一次的配置
保留键值对用键值对容器保存全部成员需要分析原始成员的专用处理程序

以 {"workers":2,"workers":5} 为例,默认读取后字典中的 workers 为 5。改用本文的拒绝策略时,这份输入会失败,两个数字相同也不能通过。是否允许覆盖应由接口契约决定,而不是把“没有解析异常”当作数据没有歧义。

原始成员、有序键值对和三种接收策略的静态关系
图1:成员接收策略说明图。原始成员与有序键值对属于输入表示,默认字典、重复键异常和键值对容器对应不同选择;这不是运行截图。

字典已经生成后就找不回重复记录

object_hook 收到的是已解码对象的字典;object_pairs_hook 收到有序键值对列表,返回值替代该对象的默认结果。两者同时配置时,后者优先。因此,不能指望在 object_hook 中查看字典长度,就知道之前有几个相同名称。

我更愿意让读取入口只负责一种成员策略,再在解析成功后调用业务转换函数。这能避免维护者误以为两个 hook 会依次执行。若原来依赖 object_hook 把对象转换成领域类型,就要把去重检查与转换显式组织进同一个 pairs 钩子,并重新考虑嵌套对象的转换条件。

另外,保留全部键值对会改变对象表示。使用简单列表保存 pairs 后,空对象和空数组可能都变成空列表;若业务要区分两者,需要明确的容器类型约定。本文选择返回字典,保持正常配置的使用方式。

用一个按对象生效的钩子拒绝重复键

下面的 unique_object 在每次回调中创建独立的 result,用 key in result 判断重复,再保存值。load_config 额外要求顶层为对象,这是示例应用自己的配置约定。它没有检查字段值类型或必填项,后续业务规则仍需另行实现。

import json


class DuplicateKeyError(ValueError):
    """输入对象包含重复成员名。"""


def unique_object(pairs):
    # 每个 JSON 对象都有自己的字典,同名键可以存在于不同对象中。
    result = {}
    for key, value in pairs:
        # 必须检查成员存在性,不能用值是否为 None 判断。
        if key in result:
            raise DuplicateKeyError("JSON 对象包含重复键")
        result[key] = value
    return result


def load_config(text):
    # 钩子的返回值替代该对象的默认字典结果,嵌套对象也适用。
    data = json.loads(text, object_pairs_hook=unique_object)
    if not isinstance(data, dict):
        raise ValueError("配置顶层必须是对象")
    return data


if __name__ == "__main__":
    # 故意重复的演示输入;读者可换成自己的小型配置文本。
    text = '{"queue": "reports", "workers": 2, "workers": 5}'
    try:
        config = load_config(text)
    except json.JSONDecodeError as exc:
        # 语法错误带位置;不记录整段配置,以免暴露字段值。
        print(f"JSON 语法错误:行 {exc.lineno},列 {exc.colno}")
    except DuplicateKeyError as exc:
        # 自定义异常与语法错误分开处理,不承诺提供完整字段路径。
        print(str(exc))
    except ValueError as exc:
        print(str(exc))
    else:
        print(config)

上面的主程序应进入重复键异常分支;若移除第二个 workers 成员,解析成功后才会打印配置。这里只给出预期,未把示例描述为实际执行记录。错误消息刻意不回显完整键名或值;若产品需要精确字段路径,单靠这个钩子的参数并不够,不能凭空补出行列信息。

unique_object 局部字典和嵌套对象之间的作用域关系
图2:钩子作用域结构图。顶层对象、嵌套对象各有自己的回调上下文,unique_object 的 result 只记录当前对象的成员;不同对象中的同名成员不构成这里的重复。

用小样本约定迁移后的结果

下面这些是接入方可以采用的预期样本,不是性能数据,也不是已经运行过的测试报告。检查时要保留原始 JSON 文本;若先交给另一个解析器转成字典,重复信息可能已被抹掉。

原始输入本文入口的预期原因
{"workers":2}接受成员唯一,顶层为对象
{"workers":null,"workers":5}拒绝null 仍然代表该成员已经出现
{"task":{"workers":2,"workers":5}}拒绝嵌套对象也经过同一钩子
{"left":{"workers":2},"right":{"workers":5}}接受同名成员分属两个对象
{"name":"a","na\u006de":"b"}拒绝转义解码后两个成员名相同
{"Mode":1,"mode":2}接受本函数不进行大小写归一化
[]拒绝应用要求顶层必须是对象

“转义后相同”与“业务认为相同”要分开:本文比较的是解析后的 Python 字符串,没有自动去空格、忽略大小写或做 Unicode 规范化。如果业务要合并这些名称,规则应该公开写进接口约定,并在规范化之后另行处理冲突。

分批接入,并把错误交给数据生产者

对我来说,这个方案最实际的代价是兼容性:它让过去被静默覆盖的输入开始报错。我会先用脱敏的小样本说明变化,再让各生产方修复生成逻辑;服务端把这类错误映射为可理解的输入错误,避免在失败后自动退回默认解析,否则接收规则又变回了宽松覆盖。

  • 确定哪些入口接受原始文本,哪些入口已经拿到字典。
  • 清点既有 object_hook 的对象转换职责,避免迁移后被忽略。
  • 约定重复键、语法错误与业务字段错误的响应方式。
  • 覆盖嵌套对象、null、转义名称和顶层数组这些小样本。
  • 在上传或请求入口设置适合业务的输入大小限制;pairs 钩子本身不是流式解析器。

strict=True 会拒绝重复键吗?

不会。JSONDecoder 的 strict 参数控制字符串内部的控制字符接受规则,不能替代成员名检查。

重复的值也要报错吗?

不需要。{"primary":2,"backup":2} 的键不同。本文只拒绝同一对象里再次出现的相同键。

改成拒绝重复键就等于完整严格 JSON 吗?

不等于。数值扩展、编码和业务类型等问题有各自的处理规则。本函数的名字和接口说明应保持明确,只承诺重复键策略及顶层对象约束。

延伸阅读:Python 对象解码与重复成员说明:https://docs.python.org/3/library/json.html;JSON 成员唯一性的互操作说明:https://www.rfc-editor.org/rfc/rfc8259.html。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>