DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
晚安云;香港 CN2 ¥15/月
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →

msgpack 能打包元组键,解包却报 unhashable type: list

msgpack 把 tuple 编成数组,默认解包为不可哈希的 list。区分新数据的字符串键设计与可信旧数据的 use_list=False 兼容,避免把 strict_map_key=False 当成完整修复。

Python 字典 {(1, 2): 0} 可以交给 msgpack.packb(),生成的字节却不一定能按默认选项还原成字典。把解包参数设为 strict_map_key=False 后,仍可能出现:

TypeError: unhashable type: 'list'

这类现象在 msgpack 1.2.1 中有明确案例;正式 1.2.3 的默认规则仍相同。MessagePack 保存的是跨语言的数据类型,不会保留“这个数组原先在 Python 里是 tuple”这层信息。

两个参数分别解决两层限制

MessagePack 的 array 默认解包为 Python list。元组键在打包时变成 array,读回来便成了 list,而 list 不能作为 Python 字典的键。

默认 strict_map_key=True 还会限制 map 键只能是 str 或 bytes。把它改成 False 只是允许尝试其他类型的键,不会自动把 list 变成可哈希对象,因此可能先绕过键类型检查,再在构造字典时失败。

对自己掌控格式、来源可信且确实需要兼容的旧数据,可以同时明确使用元组数组:

import msgpack

original = {(1, 2): 0}
packed = msgpack.packb(original)
restored = msgpack.unpackb(
    packed,
    strict_map_key=False,
    use_list=False,
)

这个示例中的键只由整数构成,解包后的 tuple 可哈希。use_list=False 会影响消息里的所有数组,不只是 map 的键;调用方若原来要求列表、会修改数组内容,必须一并检查。

它也无法把任意嵌套数据变成合法字典键。键中如果仍包含 dict 等不可哈希对象,仍会报错。不能把“元组键这一个样本可读”推广成任意 MessagePack map 都能恢复成 Python dict。

新格式优先表达字段,而不是复合键

需要跨语言传输或长期保存的数据,更适合使用字符串字段名,把复合键拆成记录。例如上面的坐标和值可表示为:

records = [
    {"row": 1, "column": 2, "value": 0},
]

这种设计让接收方按业务字段验证类型,再决定是否在内存中建立元组索引。若存在重复坐标,应明确拒绝、覆盖还是合并,不能在转换成字典时静默丢掉记录。

不要简单对元组调用 str() 作为长期协议:格式不便于跨语言解析,也容易把本来不同的键编码成含混的字符串。修改已有持久化格式时,应保留原始字节,给新格式明确的版本或入口,让旧读者不会误读新结构。

不为读取外部数据放宽所有限制

msgpack 默认限制 map 键类型包含对哈希拒绝服务风险的考虑。外部输入不应只为了“能解开”就全局关闭 strict_map_key;还应保留消息大小、数组长度、map 长度等原有边界,并对解码后的结构做业务校验。

验收要检查元组键的精确值、嵌套数组的返回类型,以及所有读取这些结果的消费者。还需保留字符串键与非法复合键的对照,确认兼容参数只用在指定的旧格式入口,不会泄漏到普通外部消息的解包流程中。