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
相关产品推荐
相关产品推荐

