You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python使用frozen attrs类作为字典键的attrs/cattrs序列化问题

问题原因

cattrs默认序列化attrs类时会将其转为字典结构,而字典无法作为另一个字典的键,因此触发了不可哈希类型的报错。

实现方案

可以通过cattrs提供的自定义钩子完成转换,整个流程仅在序列化/反序列化阶段生效,业务逻辑中不需要修改dict的键类型,还是可以直接使用AbstractID实例。

方案1:cattrs 2.3+ 通用写法(推荐)

2.3及以上版本的cattrs提供了专门的字典键序列化/反序列化钩子,只要注册一次就可以全局生效处理所有以AbstractID为键的字典:

from typing import Optional, Dict
import attr
from cattrs import Converter

# 原类型定义不变
@attr.s(frozen=True)
class AbstractID:
    _id: Optional[int] = attr.ib()

    def __str__(self) -> str:
        if self._id is not None:
            return f"A{self._id}"
        else:
            return "—"

@attr.s(auto_attribs=True)
class Database:
    storage: Dict[AbstractID, str] = {}

# 初始化独立的converter,避免污染全局配置
converter = Converter()

# 注册AbstractID基础序列化/反序列化钩子
converter.register_unstructure_hook(AbstractID, lambda aid: aid._id)
converter.register_structure_hook(AbstractID, lambda val, _: AbstractID(val))

# 注册字典键专用转换钩子(适配JSON键必须为字符串的规范)
converter.register_dict_key_unstructure_hook(AbstractID, lambda aid: str(aid._id))
converter.register_dict_key_structure_hook(AbstractID, lambda key_str: AbstractID(int(key_str)))

# 测试序列化
db = Database()
db.storage[AbstractID(1)] = "some data"
unstructured = converter.unstructure(db)
print(unstructured) # 输出:{'storage': {'1': 'some data'}}

# 测试反序列化
restructured_db = converter.structure(unstructured, Database)
print(restructured_db.storage[AbstractID(1)]) # 输出:some data

方案2:兼容旧版本cattrs写法

如果使用的是2.3以下版本的cattrs,可以单独针对Dict[AbstractID, str]类型注册钩子实现转换:

from typing import Optional, Dict, Any
import attr
from cattrs import Converter

@attr.s(frozen=True)
class AbstractID:
    _id: Optional[int] = attr.ib()

    def __str__(self) -> str:
        if self._id is not None:
            return f"A{self._id}"
        else:
            return "—"

@attr.s(auto_attribs=True)
class Database:
    storage: Dict[AbstractID, str] = {}

converter = Converter()
converter.register_unstructure_hook(AbstractID, lambda aid: aid._id)
converter.register_structure_hook(AbstractID, lambda val, _: AbstractID(val))

# 自定义Dict[AbstractID, str]的序列化逻辑
def unstructure_aid_dict(data: Dict[AbstractID, Any]) -> Dict[str, Any]:
    return {str(converter.unstructure(k)): converter.unstructure(v) for k, v in data.items()}
converter.register_unstructure_hook(Dict[AbstractID, str], unstructure_aid_dict)

# 自定义Dict[AbstractID, str]的反序列化逻辑
def structure_aid_dict(data: Dict[str, Any], _) -> Dict[AbstractID, str]:
    return {converter.structure(int(k), AbstractID): converter.structure(v, str) for k, v in data.items()}
converter.register_structure_hook(Dict[AbstractID, str], structure_aid_dict)

# 测试逻辑和方案1完全一致
db = Database()
db.storage[AbstractID(1)] = "some data"
unstructured = converter.unstructure(db)
restructured_db = converter.structure(unstructured, Database)
print(restructured_db.storage[AbstractID(1)])
注意事项
  • 建议使用独立的Converter实例而非全局cattrs对象,避免自定义钩子影响其他不需要该转换的业务逻辑。
  • 序列化时将id转为字符串是为了适配JSON规范的要求(JSON的键只能是字符串类型),如果你的序列化目标不是JSON,也可以直接用int作为键存储。

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 21:54:02