跳转到内容
新建笔记

Python pickle:对象图序列化与兼容边界

pickle 把 Python 对象图转换为字节流,并在兼容的 Python 环境中恢复对象。它可以保留列表之间的共享引用、循环引用,以及许多 Python 类型,适合来源可信、环境受控的中间结果和缓存。

只反序列化可信且未被篡改的 pickle 数据。 加载过程可能执行对象恢复逻辑,恶意数据能够触发任意代码执行;捕获异常、检查 .pkl 后缀或改用另一个 pickle 协议都不能消除这个边界。Python pickle 文档

本文示例使用 Python 3.11,所有加载的字节都在同一示例中由程序自身生成。

内存字节与二进制文件

跳转到“内存字节与二进制文件”
接口作用
pickle.dumps(obj, protocol=...)返回表示对象的 bytes
pickle.loads(data)从字节内容恢复对象
pickle.dump(obj, file, protocol=...)向二进制文件写入表示
pickle.load(file)从二进制文件读取一个表示并恢复对象

dump、load 使用 wb、rb 等二进制模式。读取时会自动识别协议版本,不需要再传 protocol。

下面先生成数据,再写入临时目录、读取并核对结果。版本字段表示应用的数据布局版本,与 pickle 协议号不是一回事。

import pickle
from pathlib import Path
from tempfile import TemporaryDirectory
data = {
"schema_version": 1,
"samples": [1.5, 2.0, 3.25],
"shape": (3,),
"flags": {"calibrated", "complete"},
}
blob = pickle.dumps(data, protocol=5)
assert isinstance(blob, bytes)
restored = pickle.loads(blob)
assert restored == data
assert isinstance(restored["shape"], tuple)
assert isinstance(restored["flags"], set)
with TemporaryDirectory() as directory:
path = Path(directory) / "measurements.pkl"
with path.open("wb") as file:
pickle.dump(data, file, protocol=5)
with path.open("rb") as file:
loaded = pickle.load(file)
assert loaded["schema_version"] == 1
assert loaded == data

程序自己曾经写过一个文件,不代表这个路径此后永远可信;实际使用中还要清楚谁能替换文件。外部接口传入的普通数据通常适合按约定使用 JSON 等数据格式,再验证字段。

共享关系和循环关系会被恢复

跳转到“共享关系和循环关系会被恢复”

恢复的是一个新的对象图,不是原进程的内存地址。图内两个位置共同引用一个列表的关系,可以继续成立。

import pickle
shared = [1, 2]
source = {"left": shared, "right": shared}
source["self"] = source
restored = pickle.loads(pickle.dumps(source, protocol=5))
assert restored is not source
assert restored["left"] is restored["right"]
assert restored["left"] is not shared
assert restored["self"] is restored
restored["left"].append(3)
assert restored["right"] == [1, 2, 3]
assert shared == [1, 2]

这与 deepcopy 的 memo 机制 有相似目的,但不能据此把 pickle 往返当作所有对象都适用的深拷贝:两者都有各自的对象协议、支持范围和执行行为。

协议兼容与代码依赖分别检查

跳转到“协议兼容与代码依赖分别检查”

Python 3.11 支持协议 0–5,默认协议是 4;协议 5 从 Python 3.8 开始提供。显式写 protocol=5 表明读取环境至少要支持它。HIGHEST_PROTOCOL 表示当前解释器支持的最高协议,不能据此保证旧环境也能读取。数据流格式

协议匹配只是一个条件。自定义类通常按“模块名和类名”定位,pickle 文件不自动包含整个类定义、依赖包和源代码。函数通常也按可导入名称引用,而不是把函数体打包进去;局部函数和普通 lambda 不适合直接按这一机制保存。可序列化对象

环境变化可能的影响处理思路
读取解释器不支持协议无法解码按最低读取版本选协议
类被移到其他模块或改名找不到原来的对象定义保留兼容导入路径,或设计迁移过程
库升级改变状态格式载入失败或含义发生变化同时记录依赖版本和应用 schema
文件截断、损坏抛出加载异常将缓存视为可重建结果,保留原始数据来源
保存了外部资源状态文件句柄、连接等不能自动恢复为等价资源存描述信息,在合适阶段重新建立资源

支持某个协议,不代表所有依赖对象跨版本都兼容。长期交换的数据最好先明确一份独立于 Python 类实现的数据 schema。

文件边界、错误和持久化能力

跳转到“文件边界、错误和持久化能力”

loads 读取第一个完整 pickle 对象后会忽略尾随字节。因此“能加载”不能证明整个文件只包含一条记录,也不能充当文件完整性验证。连续写多个对象可以形成应用约定的记录流,但读方也必须按这个约定逐条读取。

加载异常不限于 UnpicklingError;截断数据可能出现 EOFError,对象或模块缺失可能出现 AttributeError、ImportError 等。应用应区分“缓存可以丢弃后重新计算”和“唯一数据必须保留并诊断”,不要只因捕获了一个异常就认定数据有效。

pickle 本身不提供加密、并发写入控制、原子更新或事务。这里只完成对象到字节的转换;需要可靠文件更新时,应在外层设计写入流程。需要固定字段宽度和明确字节序的设备协议,则使用 struct。