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 长度等原有边界,并对解码后的结构做业务校验。
验收要检查元组键的精确值、嵌套数组的返回类型,以及所有读取这些结果的消费者。还需保留字符串键与非法复合键的对照,确认兼容参数只用在指定的旧格式入口,不会泄漏到普通外部消息的解包流程中。


