Chapter 53: Serialization Boundary
序列化边界决定一个 Python 对象离开当前进程时还能保留哪些语义。读完本章后,读者应能定位一次序列化操作的边界,判断它保留的是跨语言值结构、Python 对象图、CPython 内部格式,还是手写二进制布局,并能解释反序列化时的安全、兼容和对象身份后果。
本章的贯穿材料是一份任务快照。它包含普通字段、时间、十进制金额、二进制 payload、共享引用和一个轻量自定义对象。这个对象在进程内可以通过引用、类型和方法正常工作;一旦写入文件、发送到网络或交给另一个解释器,它就必须被压成某种外部表示。外部表示的选择会改变恢复结果。
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
@dataclass
class JobSnapshot:
job_id: str
created_at: datetime
amount: Decimal
payload: bytes
tags: list[str]
shared_tags = ["python", "runtime"]
snapshot = JobSnapshot(
job_id="J-2026-0001",
created_at=datetime(2026, 6, 7, 10, 30, tzinfo=timezone.utc),
amount=Decimal("19.90"),
payload=b"\x01\x02\x03",
tags=shared_tags,
)
package = {"first": snapshot, "second_tags": shared_tags}
这段代码中的关键点不在字段数量,而在对象关系。snapshot.tags 和 package["second_tags"] 指向同一个 list;created_at、amount、payload 和 JobSnapshot 都携带 Python 类型语义。序列化方案需要回答四个问题:外部表示支持哪些值,是否能恢复 Python 类型,是否保留对象共享关系,加载动作会执行哪些逻辑。
本章以 CPython 标准库为主。json、pickle、marshal 和 struct 的行为以 Python 3.14 文档为版本边界:json 文档把 JSON 定义为轻量数据交换格式,并说明默认支持的 Python 到 JSON 转换表;pickle 文档说明它是 Python 对象结构到字节流的二进制协议,并明确 unpickle 可信边界;marshal 文档说明它服务 .pyc 等内部格式,格式和 code object 兼容性受 Python 版本约束;struct 文档说明手写二进制布局时要显式处理字节序、大小和对齐。相关官方页面分别是 json 文档、pickle 文档、marshal 文档 和 struct 文档。
53.1 json
json 的边界是跨语言值结构。它把 Python 中可表达为 JSON 的值转换成文本:object、array、string、number、true、false、null 对应 Python 的 dict、list、str、int、float、True、False、None。这个边界适合配置、HTTP API、事件日志和长期可读数据,因为接收方只需要理解 JSON 数据模型。
把贯穿材料直接交给 json.dumps() 会失败,因为 datetime、Decimal、bytes 和自定义 dataclass 不在默认转换表内。这个失败是边界信号:对象在进程内拥有丰富类型,在 JSON 边界上需要先被投影成普通值。
import json
json.dumps(package)
这段代码会在遇到 JobSnapshot 时抛出 TypeError。json 不会自动保存类路径、构造函数、descriptor、方法或对象身份;它只处理 JSON 能表达的值。自定义对象进入 JSON 边界前,应先变成显式 schema。
import base64
import json
wire_snapshot = {
"job_id": snapshot.job_id,
"created_at": snapshot.created_at.isoformat(),
"amount": str(snapshot.amount),
"payload_b64": base64.b64encode(snapshot.payload).decode("ascii"),
"tags": list(snapshot.tags),
}
wire_package = {
"first": wire_snapshot,
"second_tags": list(shared_tags),
}
data = json.dumps(wire_package, ensure_ascii=False, sort_keys=True)
restored = json.loads(data)
这个版本的核心动作是显式降级。datetime 变成 ISO 8601 字符串,Decimal 变成十进制字符串,bytes 变成 base64 文本,dataclass 变成 dict。恢复时,调用方需要根据 schema 手动把字段升回目标类型。
from datetime import datetime
from decimal import Decimal
restored_snapshot = JobSnapshot(
job_id=restored["first"]["job_id"],
created_at=datetime.fromisoformat(restored["first"]["created_at"]),
amount=Decimal(restored["first"]["amount"]),
payload=base64.b64decode(restored["first"]["payload_b64"]),
tags=list(restored["first"]["tags"]),
)
这段恢复代码说明 JSON 边界的责任分布:json.loads() 只恢复 JSON 值结构,业务代码负责解释字段含义。这个分工带来可移植性,也带来 schema 演进责任。新增字段、字段重命名、时间格式、金额精度和二进制编码都需要由协议层明确。
json 对对象共享关系的处理也很直接。wire_package["first"]["tags"] 和 wire_package["second_tags"] 在写入前已经是两个 list;即使它们内容相同,JSON 文本里也只是两段数组值。读取后得到两个独立 list。缓存、对象池、共享状态、循环引用都无法靠 JSON 默认结构表达。
json_restored = json.loads(json.dumps({"a": shared_tags, "b": shared_tags}))
print(json_restored["a"] == json_restored["b"])
print(json_restored["a"] is json_restored["b"])
输出中第一个判断为 True,第二个判断为 False。JSON 保留值相等,不保留 Python 对象身份。需要表达共享关系时,应使用显式 ID,例如把 tags 写成 {"id": "tags-1", "items": [...]},再让多个位置引用同一个 ID。
安全方面,JSON 解析本身不会按 Python 类路径执行构造逻辑。它的主要风险来自资源消耗、巨大数字、深层嵌套、超大字符串和业务层错误解释。Python 3.11 起,默认整数解析受解释器整数字符串长度限制影响,这有助于降低极端长整数字符串造成的拒绝服务成本。读取不可信 JSON 时,稳定做法是先限制输入大小和嵌套,再校验 schema,最后执行类型转换。
53.2 pickle
pickle 的边界是 Python 对象图。它把对象层级转换成字节流,并在加载时重建对象层级。官方文档把这个过程称为 pickling 和 unpickling,并明确说明 pickle 是 Python-specific 的二进制格式。它适合可信边界内的缓存、进程间传递、实验数据快照和内部持久化。
贯穿材料交给 pickle 时,默认会尝试保存 dataclass 实例、datetime、Decimal、bytes 和共享 list。它比 JSON 保留更多 Python 语义,但代价是接收端必须运行兼容的 Python 环境,并且能找到相同模块路径下的类定义。
import pickle
blob = pickle.dumps(package, protocol=pickle.HIGHEST_PROTOCOL)
restored_package = pickle.loads(blob)
print(restored_package["first"].amount)
print(restored_package["first"].tags is restored_package["second_tags"])
第二个判断通常为 True。pickle 使用 memo 记录已经见过的对象,让共享对象和递归对象按引用关系写入流中。这个 memo 是理解 pickle 对象图能力的关键:同一个对象第二次出现时,字节流可以引用前面已经写出的对象位置。
cycle = []
cycle.append(cycle)
restored_cycle = pickle.loads(pickle.dumps(cycle))
print(restored_cycle[0] is restored_cycle)
这个例子显示 pickle 能保存循环引用。JSON 默认路径会在循环容器上失败,因为 JSON 数据模型没有“引用已出现对象”的内建表示。对象图越依赖共享引用、递归结构和 Python 类,pickle 的语义优势越明显。
pickle 的恢复能力依赖类路径。对于普通类实例,pickle 流通常记录“模块名 + 全局名字”,加载时通过导入模块并查找名字来得到类对象。类被移动、重命名、删除,或者接收端环境缺少对应模块时,unpickle 会失败,常见异常包括 AttributeError、ImportError 和 ModuleNotFoundError。
# 伪代码:展示类路径依赖,不要求在当前文件中运行。
# 写入端保存 myapp.models.JobSnapshot 的实例。
# 读取端需要仍然能导入 myapp.models,并在其中找到 JobSnapshot。
这个依赖决定了 pickle 的版本演进方式。内部缓存可以跟随部署一起失效;长期文件、跨服务协议和外部客户数据需要更稳定的 schema。把 pickle 用作长期存档时,开发者通常要控制类的 __getstate__()、__setstate__() 或 __reduce__(),并在状态中写入显式版本号。
安全边界是 pickle 的核心限制。官方文档明确说明 pickle 具备任意代码执行风险,加载数据时只应处理可信来源。风险来自 unpickle 过程会根据流中的指令查找全局对象、调用重建逻辑并恢复状态。攻击者能构造恶意 pickle,让加载动作触发不期望的函数调用。
# 风险示意:不要加载用户上传、网络来源或可被篡改的 pickle。
# 稳定做法:可信内部缓存可以用 pickle;外部输入优先选择 JSON 或显式协议。
pickle 的工程判断顺序是先确认可信边界,再确认 Python 环境和类路径,再确认协议版本,最后确认对象图语义。只要第一步不成立,后续性能、便利性和对象图能力都不能抵消加载风险。
53.3 marshal
marshal 的边界是 CPython 内部值格式。它能把部分 Python 值写成二进制格式,也能读取回来,但它的设计目标主要是支持 .pyc 文件中的 pseudo-compiled code。官方文档说明 marshal 格式细节有意不公开,Python 维护者保留以不向后兼容方式修改格式的权利。
对贯穿材料来说,marshal 并不适合作为业务序列化方案。它不支持任意自定义类实例;它支持的类型集中在与一次 Python 调用无关的值,例如数字、字符串、bytes、若干容器、部分单例,以及在允许时处理 code object。Python 3.14 中 marshal format version 5 增加了 slice 支持,这也说明它的版本演进紧贴解释器内部需要。
import marshal
simple = {
"job_id": snapshot.job_id,
"payload": snapshot.payload,
"tags": tuple(snapshot.tags),
}
raw = marshal.dumps(simple)
restored_simple = marshal.loads(raw)
这个例子能运行的原因是 simple 只保留 marshal 支持的值。JobSnapshot、datetime 和 Decimal 的对象语义没有进入这个边界。把完整 snapshot 交给 marshal.dumps() 会触发类型支持问题。
marshal.dumps(snapshot)
这段代码会失败,因为自定义 dataclass 实例不属于 marshal 的一般支持范围。这个失败应被读成用途边界,而非功能缺口。marshal 服务解释器内部格式,业务持久化应选择 JSON、pickle、数据库、消息协议或专门的二进制格式。
.pyc 相关场景能说明 marshal 的位置。CPython 编译 .py 时会得到 code object;.pyc 文件内部会包含 header 和 marshalled code object。这个格式和解释器版本、code object 结构、字节码布局关联紧密。跨 Python 版本直接反序列化 code object 可能产生未定义行为。
# 观察方向:这个片段只展示 marshal 与 code object 的关系。
code = compile("x = 1\ny = x + 2\n", "<demo>", "exec")
raw_code = marshal.dumps(code, allow_code=True)
restored_code = marshal.loads(raw_code, allow_code=True)
allow_code 是 Python 3.13 起加入的参数,用来控制是否允许 code object 进入 marshal 读写路径。这个参数体现了 marshal 的安全与内部格式边界:code object 本身携带可执行指令,读取来源需要可信。对于普通应用数据,把 code object 序列化进文件会扩大恢复路径的风险和版本耦合。
53.4 object graph serialization
对象图序列化要处理“值”之外的关系。对象图由节点和边构成:节点是对象,边是引用。Python 进程内的 list、dict、自定义实例、闭包、缓存和循环结构都可能形成对象图。序列化边界要决定哪些节点能写出,哪些边能恢复,哪些类型身份需要保留。
贯穿材料中有一个共享边:snapshot.tags 与 package["second_tags"] 指向同一 list。JSON 把它变成两个值相等的数组;pickle 用 memo 保留这条共享边;marshal 对支持类型的递归容器有版本相关支持,但不负责一般 Python 对象图;手写二进制格式通常需要开发者自己设计 ID 表。
这张图的重点是同一个 shared_tags 节点有两个入边。值序列化会把节点内容复制到两个位置;对象图序列化会把第一次出现的节点记录下来,后续位置写成对该节点的引用。恢复后是否还共享,取决于外部格式是否表达了这个引用关系。
对象图序列化还要处理类型身份。JobSnapshot 的类型身份包括模块路径、类名、字段约定和可能的自定义恢复逻辑。JSON 需要显式写入 "type": "JobSnapshot" 或 schema version,再由业务代码选择构造函数。pickle 默认使用 Python 的全局可导入对象查找。二进制协议通常会用 type code 或 schema ID。
版本演进是对象图恢复的长期成本。对象新增字段时,旧数据缺少新字段;字段删除时,旧数据带着多余状态;类被移动时,pickle 的模块路径失效;字段含义变化时,JSON 的 schema 名称虽然没变,业务语义已经变化。稳定做法是在可长期保存的状态中写入版本号,并让恢复代码以版本号驱动迁移。
def snapshot_to_state(obj: JobSnapshot) -> dict[str, object]:
return {
"version": 1,
"job_id": obj.job_id,
"created_at": obj.created_at.isoformat(),
"amount": str(obj.amount),
"payload_b64": base64.b64encode(obj.payload).decode("ascii"),
"tags": list(obj.tags),
}
这个函数展示了可演进状态的最低形状。它没有依赖类的内存布局,也没有隐式依赖某个类路径。恢复逻辑可以读取 version,根据不同版本执行字段填充、默认值补齐和格式迁移。
对象图还会遇到无法稳定写出的节点。打开的文件句柄、socket、锁、线程、生成器、数据库连接和某些 C 扩展对象都携带运行时资源。它们的意义依赖当前进程、操作系统句柄或外部连接状态。此类对象进入序列化边界时,应写出可恢复配置或业务状态,例如文件路径、偏移量、连接参数和事务 ID,再在恢复端重新获取资源。
53.5 security implications
序列化安全问题来自反序列化动作的能力范围。读取字节或文本时,解析器会分配对象、递归构建容器、查找类型、调用构造逻辑或填充状态。能力范围越接近 Python 运行时,攻击面越大。安全判断要看来源、格式、解析动作和恢复后的业务使用。
json 的主要风险是资源消耗和业务解释。巨大的输入、深层嵌套、重复字段、超长数字和大字符串会消耗 CPU 或内存;业务代码在 schema 校验前使用字段,可能触发越权、路径穿越或金额精度错误。稳定顺序是限制输入体积,解析 JSON,执行 schema 校验,转换字段类型,再进入业务逻辑。
def load_snapshot_from_json(data: str) -> JobSnapshot:
if len(data) > 64 * 1024:
raise ValueError("payload too large")
raw = json.loads(data)
first = raw["first"]
return JobSnapshot(
job_id=str(first["job_id"]),
created_at=datetime.fromisoformat(first["created_at"]),
amount=Decimal(str(first["amount"])),
payload=base64.b64decode(first["payload_b64"], validate=True),
tags=[str(item) for item in first["tags"]],
)
这段代码的安全意义在于分层。长度检查控制资源边界,JSON 解析只得到基础值,类型转换集中在一个入口,base64 使用 validate=True 收紧输入形态。真实工程还会增加字段白名单、范围检查和 schema version 检查。
pickle 的主要风险是加载过程能够执行 Python 对象重建逻辑。只要输入来源不可信,pickle.loads() 本身就是危险边界。签名只能证明数据未被签名后篡改,不能把不可信生产者变成可信生产者。内部缓存、同一部署单元内的临时文件和受权限保护的进程通信可以使用 pickle;用户上传、公共网络、跨组织接口和长期开放文件格式应使用显式数据格式。
marshal 的风险同时包括安全和版本耦合。它处理内部格式,能涉及 code object,并且文档明确提醒不面向恶意数据。读取来自外部的 marshal 数据会把应用暴露给解释器内部格式边界。即使数据没有恶意,跨版本读取 code object 也可能失败或产生未定义行为。
手写二进制格式的风险来自边界检查和解释一致性。长度字段、偏移、字节序、对齐、压缩块和嵌套表都可能造成越界读取、超大分配或错误解释。Python 层使用 struct.unpack() 时会检查 buffer 大小和字段范围,但协议层仍然要限制总长度、字段数量和版本号。
import struct
HEADER = struct.Struct("!4sH I") # magic, version, payload_length
magic, version, payload_length = HEADER.unpack(b"JSNP\x00\x01\x00\x00\x00\x10")
if magic != b"JSNP" or version != 1 or payload_length > 64 * 1024:
raise ValueError("invalid header")
这里的 ! 表示 network byte order,也就是大端。把字节序写进格式字符串,可以让读取端在不同机器上得到一致解释。安全处理的重点是先读固定头部,再验证 magic、版本和长度,之后才读取可变 payload。
53.6 protocol version
协议版本决定同一类格式能表达哪些特性,以及哪个运行时能读取。pickle 的 protocol version 直接影响 opcode 集、buffer 支持、性能特征和跨版本兼容范围。Python 3.14 文档说明当前有 6 个 pickle protocol;协议号越高,读取端需要越新的 Python。
pickle.HIGHEST_PROTOCOL 表示当前解释器支持的最高协议,pickle.DEFAULT_PROTOCOL 表示默认使用协议。Python 3.14 起默认协议是 5,协议 5 在 Python 3.8 引入,支持 out-of-band buffer,并改善 in-band 数据性能。面向多个 Python 版本时,写入端应显式指定协议,而非依赖默认值。
# 面向 Python 3.8+ 的内部数据,显式使用 protocol 5。
blob_v5 = pickle.dumps(package, protocol=5)
# 面向更旧 Python 3 读取端时,需要选择读取端支持的协议。
blob_v4 = pickle.dumps(package, protocol=4)
协议版本和对象状态版本是两个层级。pickle protocol 5 说明字节流使用哪套 pickle opcode;{"version": 1} 说明业务对象状态是哪套字段约定。一个系统可以使用 pickle protocol 5,同时在对象状态里写 state_version=3。这两个版本分别解决读取器能力和业务 schema 演进问题。
JSON 本身也需要协议版本,只是版本通常由业务 schema 承载。JSON 文本能被许多语言解析,但字段含义、必选字段、默认值和枚举范围仍然需要版本。稳定的 JSON 消息通常在顶层包含 schema_version、type 或 event_name。
wire_event = {
"schema_version": 1,
"type": "JobSnapshotCreated",
"snapshot": snapshot_to_state(snapshot),
}
marshal 的 marshal.version 表示当前格式版本。Python 3.14 中 marshal version 5 增加了 slice 支持。这个版本号服务解释器内部格式演进,不能替代业务协议版本。把 marshal 用在应用数据上会让应用协议跟着解释器内部格式变化承担成本。
手写二进制格式应在 header 中放 magic 和 version。magic 用来识别文件类型,version 用来选择解析器,长度字段用来控制读取范围。没有版本号的二进制格式很难安全演进,因为读取端无法知道同一偏移处的字段含义是否已经变化。
53.7 object identity preservation
对象身份保留回答的是“恢复后两个位置是否仍指向同一个对象”。Python 中 is 观察对象身份,== 观察值相等。序列化边界会把这两个判断拆开。许多数据交换场景只需要 == 成立;缓存恢复、图结构、共享状态和循环结构常常还需要身份关系成立。
贯穿材料可以直接对比 JSON 和 pickle。
json_loaded = json.loads(json.dumps({"a": shared_tags, "b": shared_tags}))
print(json_loaded["a"] == json_loaded["b"])
print(json_loaded["a"] is json_loaded["b"])
pickle_loaded = pickle.loads(pickle.dumps({"a": shared_tags, "b": shared_tags}))
print(pickle_loaded["a"] == pickle_loaded["b"])
print(pickle_loaded["a"] is pickle_loaded["b"])
JSON 路径得到两个独立 list。pickle 路径恢复同一个共享 list。这个差异会影响恢复后的修改语义:如果修改 pickle_loaded["a"].append("core"),pickle_loaded["b"] 也能看到;JSON 路径下两个 list 独立变化。
身份保留还会影响缓存语义。进程内缓存可能用对象身份表示“同一份配置”“同一个 session”“同一节点”。如果序列化后身份丢失,恢复出来的多个对象虽然值相等,但缓存命中、对象池、图遍历去重和引用一致性都会变化。
用 JSON 表达身份关系时,需要把隐式引用改成显式引用。常见做法是把节点表和边表分开:节点表给每个对象分配 ID,边表只写 ID。恢复时先创建节点,再按 ID 连接引用。
wire_graph = {
"nodes": {
"tags-1": ["python", "runtime"],
"snapshot-1": {
"job_id": "J-2026-0001",
"tags_ref": "tags-1",
},
},
"root": {
"first_ref": "snapshot-1",
"second_tags_ref": "tags-1",
},
}
这段结构把对象身份从运行时指针变成协议 ID。接收端可以根据 ID 恢复共享关系。它比直接嵌套 JSON 更啰嗦,但语义显式,跨语言也能实现。
pickle 能自动保存 Python 对象图身份,但自动保存也意味着格式绑定 Python 对象模型。选择时要看身份关系是否属于业务协议。属于业务协议时,用显式 ID 更稳定;只属于内部缓存时,pickle 的 memo 机制更省成本。
53.8 binary serialization
二进制序列化的边界是字节布局。它关注字段占多少字节、使用什么字节序、是否带 padding、buffer 所有权如何转移,以及读取端能否按同一布局解释。这个边界常见于网络协议、文件格式、共享内存、C 扩展、传感器数据和大数组传输。
Python 的 struct 模块可以把 Python 值和 C struct 风格的 bytes 相互转换。格式字符串的第一个字符决定字节序、大小和对齐。@ 使用本机格式、本机大小和本机对齐;< 使用小端、标准大小、无自动对齐;> 使用大端、标准大小、无自动对齐;! 表示网络字节序,也就是大端。
from struct import Struct
HEADER = Struct("!4sH I")
header = HEADER.pack(b"JSNP", 1, len(snapshot.payload))
magic, version, payload_length = HEADER.unpack(header)
这个 header 的布局固定为 magic 4 字节、version 2 字节、payload_length 4 字节。! 让不同平台按照同一字节序读写。对于跨机器文件和网络协议,显式字节序通常比默认 native 模式更稳定。
alignment 会影响 native 格式大小。struct 文档说明 native 模式会根据构建解释器的平台和编译器处理对齐,可能插入 pad bytes。标准格式不会自动插入 padding。跨平台协议应把 padding 写成明确字段,例如用 x 表示 pad byte,或者完全使用标准大小格式。
native = struct.calcsize("@cI")
standard = struct.calcsize("!cI")
print(native, standard)
这段代码在不同平台上可能得到不同的 native,但 standard 的大小固定。二进制协议设计时,calcsize() 是校验布局的工具;协议文档应记录每个字段的大小、字节序和范围。
buffer ownership 是二进制序列化的另一个核心问题。bytes 表示不可变字节副本;bytearray 表示可变字节数组;memoryview 可以把底层 buffer 暴露给读写函数,减少中间复制。struct.pack_into() 可以直接写入可写 buffer,struct.unpack_from() 可以从已有 buffer 的偏移处读取。
buffer = bytearray(HEADER.size + len(snapshot.payload))
HEADER.pack_into(buffer, 0, b"JSNP", 1, len(snapshot.payload))
buffer[HEADER.size:] = snapshot.payload
view = memoryview(buffer)
magic, version, payload_length = HEADER.unpack_from(view, 0)
payload_view = view[HEADER.size:HEADER.size + payload_length]
这里的 payload_view 共享 buffer 的底层内存。只要 buffer 仍然存在,payload_view 就能读取对应范围。这个关系带来低复制成本,也带来生命周期责任:生产者、消费者和持有 memoryview 的代码要约定何时释放、何时可修改、谁拥有底层 buffer。
pickle protocol 5 的 out-of-band buffer 也属于二进制边界问题。它把对象图描述和大块 buffer 数据分离,让通信系统可以用共享内存或专门通道传输大数据。这个能力适合大数组、图像帧和数值计算数据,但需要发送端和接收端同时遵守 buffer 顺序和生命周期约定。
二进制序列化的检查顺序是先看 magic 和版本,再看 header 长度,再看字节序和字段大小,再看可变长度字段边界,最后看 buffer 所有权。这个顺序能把格式识别、兼容判断、越界风险和性能成本分开处理。
最小自检任务
阅读下面代码,判断 JSON 路径和 pickle 路径恢复后各自保留了哪些语义,并说明哪个路径适合接收外部用户上传的数据。
import json
import pickle
shared = ["a", "b"]
obj = {"left": shared, "right": shared}
json_obj = json.loads(json.dumps(obj))
pickle_obj = pickle.loads(pickle.dumps(obj))
print(json_obj["left"] == json_obj["right"])
print(json_obj["left"] is json_obj["right"])
print(pickle_obj["left"] == pickle_obj["right"])
print(pickle_obj["left"] is pickle_obj["right"])
答案要点
JSON 路径保留 list 的值结构,所以 json_obj["left"] == json_obj["right"] 为 True;它没有保存 Python 对象身份,所以 json_obj["left"] is json_obj["right"] 为 False。pickle 路径保留对象图中的共享引用,所以两个 pickle 判断都为 True。外部用户上传数据应优先使用 JSON 这类显式数据格式,并在解析后执行大小限制、schema 校验和字段类型转换。pickle 只适合可信边界内,因为 unpickle 过程能够执行对象重建逻辑并带来代码执行风险。
本章知识点总结
- 序列化边界:序列化边界决定对象离开当前进程后保留值结构、对象图、内部格式还是字节布局。
- JSON 模型:
json适合跨语言值交换,默认只恢复基础值结构,复杂类型需要显式 schema 和转换代码。 - 类型降级:
datetime、Decimal、bytes和自定义对象进入 JSON 前要转换成字符串、base64、dict 或协议字段。 - pickle 对象图:
pickle能保存 Python 对象层级、共享引用和循环引用,适合可信内部持久化和缓存。 - 类路径依赖:pickle 恢复普通类实例时依赖模块路径和全局名字,类移动或重命名会破坏旧数据读取。
- marshal 边界:
marshal服务 CPython 内部格式和.pyc场景,格式和 code object 与 Python 版本强绑定。 - 对象图关系:对象图序列化要处理节点、引用边、循环、共享对象、类型身份和版本迁移。
- 安全判断:反序列化安全取决于输入来源、解析动作、资源消耗、类型查找和恢复逻辑的执行能力。
- 协议版本:pickle protocol version 约束 opcode、buffer 支持和读取端版本,业务 schema version 约束字段含义和迁移逻辑。
- 身份保留:JSON 通常保留值相等,pickle 可以保留共享身份;跨语言身份语义应使用显式 ID 表达。
- 二进制布局:手写二进制格式要固定 magic、版本、字节序、字段大小、长度边界和 padding 规则。
- buffer 所有权:
bytes、bytearray、memoryview和 out-of-band buffer 会改变复制成本与生命周期责任。 - 检查顺序:选择序列化方案时先看可信边界,再看可移植性、对象图需求、版本演进和二进制性能成本。