Python 对象持久化的多种实现方案

2次阅读

该用 pickle 时仅限可信环境内部短时传递(如 multiprocessing 参数、本地调试缓存);不该用时包括网络传输、用户输入、跨版本读取或长期存储,因其存在反序列化任意代码风险及协议兼容问题。

Python 对象持久化的多种实现方案

什么时候该用 pickle,什么时候不该碰它

picklepython 原生最顺手的对象序列化工具,但它的适用边界非常明确:只在可信环境内部使用。一旦涉及网络传输、跨版本读取或长期存储,pickle 就可能反咬一口。

常见错误现象:AttributeError: Can't get attribute 'X' on <module></module>——这是典型模块路径不一致导致的反序列化失败;或者用 Python 3.9 保存的对象,在 3.12 里加载时报 ValueError: unsupported pickle protocol

  • 只用于进程间短时传递(如 multiprocessing 的参数)、本地调试缓存
  • 永远不要加载来自用户输入、文件上传或网络响应的 pickle 数据——它会直接执行任意代码
  • 协议版本建议显式指定:pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL),避免默认用 3 或 4 协议造成低版本兼容问题
  • 自定义类必须确保 __reduce____getstate__ 行为稳定,否则升级类定义后旧数据无法恢复

json 能存对象吗?能,但得先“扁平化”

json 本身不支持 Python 对象,它只认 dictliststrintFloatboolNone。想存对象,就得自己负责“拆解”和“组装”。

使用场景:需要跨语言读取、写入配置、记录日志结构体、前端可直读的数据缓存。

立即学习Python免费学习笔记(深入)”;

  • 别直接 json.dump(obj, f)——会报 TypeError: Object of type X is not JSON serializable
  • default= 参数处理未知类型,比如把 datetime 转成 ISO 字符串json.dump(obj, f, default=Lambda x: x.isoformat() if hasattr(x, 'isoformat') else str(x))
  • 反序列化时不能自动还原类型,得手动调用构造函数,例如 MyClass(**data) 或用 object_hook
  • 注意浮点精度:json 不保留 decimal.Decimal,也不区分 intfloat,数值全按 double 解析

sqlite + sqlite3 自带的 adapt/convert 机制

当对象结构固定、需要查询能力又不想引入 ORM 时,SQLite 是个被低估的选择。Python 的 sqlite3 模块允许注册类型适配器,让自定义类进出数据库像原生类型一样自然。

性能影响:比纯文件序列化略慢,但支持索引、WHERE 查询、事务,适合中等规模结构化持久化。

  • 注册适配器前,必须先启用类型检测:sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES)
  • 适配器函数返回值只能是 strbytesintfloatNone,其他类型会被忽略
  • 转换器函数接收的是 bytesstr,需自行解析,比如用 json.loads 还原嵌套结构
  • 注意并发:多个连接同时写入时,SQLite 默认以文件锁阻塞,不是真正的线程安全

dataclasses + typing 定义 schema,再选序列化后端

真正可持续的持久化,往往始于清晰的类型契约。用 @dataclass 描述数据结构,配合 typing 注解,能让后续换序列化方案、加校验、生成文档都变得简单。

容易踩的坑:有人一上来就 pydantic,结果发现只是存几个配置项,反而增加依赖和启动开销。

  • 轻量级场景优先用 dataclasses.asdict() + json,不引入额外依赖
  • 字段含嵌套对象?先递归转成 dict,别指望 asdict 自动处理非 dataclass 类型
  • 需要字段校验或默认值行为更健壮?再考虑 pydantic.BaseModel,但注意 v2 版本的 model_dump() 替代了旧版 dict()
  • 如果未来可能迁移到数据库,dataclass 比裸 dict 更容易映射到 ORM 模型,字段名和类型信息都在那里

复杂点不在选哪个方案,而在对象生命周期里哪一环开始失去控制:是类定义改了但旧数据没迁移?还是序列化格式变了却忘了更新读取逻辑?这些地方没有银弹,只有提前约定好谁负责兼容、谁负责报错、谁来写迁移脚本。

text=ZqhQzanResources