序列化
序列化是把内存中的对象转换成可保存或可传输的数据;反序列化则把这些数据恢复为程序可以使用的对象。保存配置、调用 Web API、缓存计算结果和进程间交换数据都离不开序列化。
Python 标准库中最常见的两种方案是:
- JSON:跨语言、可读,适合接口和长期数据格式;
- pickle:Python 专用,能保存更多 Python 对象,但绝不能反序列化不可信数据。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 区分序列化、编码和压缩;
- 使用
json.dumps()、json.loads()、json.dump()和json.load(); - 处理中文、自定义类型、JSON Lines 和数值精度;
- 理解
pickle的能力、协议、兼容性和安全风险; - 根据数据来源、使用者和保存周期选择合适方案。
序列化不等于编码或压缩
这三个概念经常一起出现,但目的不同:
| 操作 | 目的 | 示例 |
|---|---|---|
| 序列化 | 把对象变为规定结构的数据 | 字典变为 JSON 文本 |
| 字符编码 | 在文本 str 与字节间转换 | UTF-8 编码和解码 |
| 压缩 | 减少数据占用空间 | gzip |
典型流程可能是:对象先序列化成 JSON 字符串,再编码成 UTF-8 字节,最后使用 gzip 压缩。反向操作的顺序必须相反。
使用 JSON
JSON 是与编程语言无关的文本格式,常用于 Web API、配置文件和消息传递。Python 使用标准库 json 处理 JSON。
Python 与 JSON 类型映射
| Python 类型 | JSON 类型 | 反序列化后的 Python 类型 |
|---|---|---|
dict | object | dict |
list、tuple | array | list |
str | string | str |
int、float | number | int、float |
True、False | true/false | bool |
None | null | None |
注意两个容易忽略的变化:
- Python 元组会被写成 JSON 数组,读回来是列表;
- JSON 对象的键必须是字符串,整数键写入后读回来会变成字符串。
因此,loads(dumps(value)) 不保证与原对象完全相等。
在字符串中转换:dumps() 和 loads()
dumps() 把 Python 对象转换为 JSON 字符串,loads() 执行反向操作:
import json
user = {
"name": "小林",
"age": 20,
"skills": ["Python", "SQL"],
"active": True,
"nickname": None,
}
json_text = json.dumps(user, ensure_ascii=False)
restored = json.loads(json_text)
print(json_text)
print(restored["name"])
函数名中的 s 可以理解为 string。dumps() 返回 str,不是 bytes;网络接口需要字节时还要根据协议编码:
payload = json_text.encode("utf-8")
读写文件:dump() 和 load()
dump() 把 JSON 写入文本文件对象,load() 从文本文件对象读取:
from pathlib import Path
import json
config = {
"theme": "dark",
"page_size": 20,
"features": ["search", "export"],
}
path = Path("config.json")
with path.open("w", encoding="utf-8") as file:
json.dump(config, file, ensure_ascii=False, indent=2)
with path.open("r", encoding="utf-8") as file:
loaded_config = json.load(file)
print(loaded_config)
JSON 文件应按约定使用 UTF-8。indent=2 让文件便于阅读,但会增加体积;机器间高频传输时通常使用紧凑格式。
常用格式化参数
import json
data = {"name": "小林", "scores": [95, 88]}
pretty = json.dumps(
data,
ensure_ascii=False,
indent=2,
sort_keys=True,
)
compact = json.dumps(
data,
ensure_ascii=False,
separators=(",", ":"),
)
| 参数 | 作用 |
|---|---|
ensure_ascii=False | 直接输出中文等非 ASCII 字符 |
indent=2 | 缩进排版,便于人工查看 |
sort_keys=True | 按键排序,便于比较和生成稳定文本 |
separators=(",", ":") | 去掉分隔符后的空格,生成更紧凑的文本 |
allow_nan=False | 遇到 NaN 或无穷大时报错,生成符合严格 JSON 规范的数据 |
Python 默认可能把浮点特殊值写成 NaN、Infinity 和 -Infinity,这些值不属于标准 JSON。要与严格解析器互操作,应使用 allow_nan=False。
处理 JSON 错误
非法 JSON 会让 loads() 或 load() 抛出 json.JSONDecodeError:
import json
text = '{"name": "小林",}' # JSON 不允许尾随逗号
try:
data = json.loads(text)
except json.JSONDecodeError as error:
print(f"JSON 格式错误:第 {error.lineno} 行,第 {error.colno} 列")
else:
print(data)
还要区分“语法正确”和“业务结构正确”。下面的 JSON 虽然合法,但字段类型未必符合程序要求:
data = json.loads('{"page_size": "很多"}')
if not isinstance(data.get("page_size"), int):
raise ValueError("page_size 必须是整数")
对外部输入,应在反序列化后继续校验必填字段、类型、范围、长度和嵌套深度。
序列化自定义对象
JSON 不认识 datetime、Decimal、Path 和普通类实例。最清晰的做法通常是先把对象转换成只包含 JSON 基础类型的数据结构。
数据类转换为字典
from dataclasses import asdict, dataclass
import json
@dataclass
class User:
name: str
age: int
user = User("小林", 20)
json_text = json.dumps(asdict(user), ensure_ascii=False)
print(json_text)
反序列化时显式构造对象,并借此完成字段检查:
data = json.loads(json_text)
restored_user = User(name=data["name"], age=data["age"])
不要直接把任意外部字典用 User(**data) 创建成可信对象;应先验证字段集合和类型。
使用 default 处理特殊类型
from datetime import datetime
import json
def encode_special(value: object) -> object:
if isinstance(value, datetime):
return {"__type__": "datetime", "value": value.isoformat()}
raise TypeError(f"无法序列化类型:{type(value).__name__}")
event = {
"name": "发布",
"created_at": datetime(2026, 9, 12, 10, 30),
}
text = json.dumps(event, default=encode_special, ensure_ascii=False)
print(text)
default 只在默认编码器无法处理对象时调用。无法识别的类型应抛出 TypeError,不能悄悄转换成含义不明确的字符串。
使用 object_hook 恢复对象
from datetime import datetime
import json
def decode_special(value: dict[str, object]) -> object:
if value.get("__type__") == "datetime":
raw_value = value.get("value")
if not isinstance(raw_value, str):
raise ValueError("datetime.value 必须是字符串")
return datetime.fromisoformat(raw_value)
return value
restored = json.loads(text, object_hook=decode_special)
print(restored["created_at"])
object_hook 会对每个 JSON 对象调用,包括嵌套对象。类型标记可能与普通业务字段冲突,因此实际协议应使用明确、稳定的模式,并验证标记与字段。
数值精度
JSON 数字没有规定 Python 的具体数值类型。默认情况下,带小数点的数字会解析为 float,不适合要求十进制精确计算的金额:
from decimal import Decimal
import json
data = json.loads(
'{"price": 19.99}',
parse_float=Decimal,
)
print(data["price"])
print(type(data["price"])) # <class 'decimal.Decimal'>
Decimal 本身不能直接由默认 JSON 编码器写出。跨系统传输金额时,常见方案是使用字符串或最小货币单位整数,并在数据协议中明确约定。
多条记录与 JSON Lines
连续多次调用 json.dump() 写入同一文件,会得到多个紧挨着的 JSON 值,整体通常不是一个合法 JSON 文档。保存多条独立记录时,可以:
- 把所有记录放进一个 JSON 数组;
- 使用 JSON Lines,每行一个完整 JSON 对象。
写入 JSON Lines:
from pathlib import Path
import json
records = [
{"name": "小林", "score": 95},
{"name": "小王", "score": 88},
]
path = Path("scores.jsonl")
with path.open("w", encoding="utf-8") as file:
for record in records:
line = json.dumps(record, ensure_ascii=False)
file.write(f"{line}\n")
逐行读取可以避免一次加载全部记录:
with path.open("r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
if not line.strip():
continue
try:
record = json.loads(line)
except json.JSONDecodeError as error:
raise ValueError(f"第 {line_number} 行不是合法 JSON") from error
print(record)
JSON Lines 是常见约定,但不是单一正式 JSON 文档;交换数据前应确认双方使用相同格式。
使用 pickle
pickle 可以序列化许多 JSON 不支持的 Python 对象,例如集合、元组、自定义类实例和共享引用。
from pathlib import Path
import pickle
data = {
"languages": {"Python", "Go"},
"point": (10, 20),
}
path = Path("data.pkl")
with path.open("wb") as file:
pickle.dump(data, file)
with path.open("rb") as file:
restored = pickle.load(file)
print(restored)
pickle 是二进制格式,所以文件必须使用 "wb" 和 "rb"。
在字节中转换
pickle.dumps() 返回 bytes,pickle.loads() 从字节恢复对象:
import pickle
payload = pickle.dumps([1, 2, 3])
restored = pickle.loads(payload)
print(payload)
print(restored)
协议版本
pickle 有多个协议版本。写入当前 Python 能提供的高效格式,可以明确使用 pickle.HIGHEST_PROTOCOL:
payload = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
协议越新,旧版 Python 越可能无法读取。需要跨 Python 版本交换时,应根据最低支持版本选择协议,并通过真实环境测试。
自定义类与代码变化
pickle 通常记录类的模块路径和名称,加载时需要能够导入对应类:
from dataclasses import dataclass
import pickle
@dataclass
class User:
name: str
age: int
payload = pickle.dumps(User("小林", 20))
restored = pickle.loads(payload)
类被移动、重命名,模块结构改变,或者属性含义变化后,旧数据可能无法加载或产生不符合新业务规则的对象。pickle 更适合短期缓存、受控环境和内部工具,不应默认作为长期数据存档格式。
确实需要控制对象状态时,可以实现 __getstate__() 和 __setstate__(),但还应设计明确的数据版本与迁移策略。长期保存的关键数据通常更适合使用带模式和迁移机制的数据库,或稳定的 JSON 等交换格式。
pickle 的安全边界
pickle.load() 和 pickle.loads() 在反序列化过程中可能执行任意代码。绝不能加载来自用户上传、网络、邮件或其他不可信来源的 pickle 数据。
签名或加密只能在正确验证真实性和完整性后降低数据被篡改的风险,不能把来源未知的 pickle 变成通用安全格式。需要处理外部数据时,使用 JSON 等只描述数据的格式,并在解析后严格校验结构。
即使 JSON 不会像 pickle 一样直接构造任意 Python 对象,超大输入、极深嵌套和业务字段滥用仍可能消耗资源或触发逻辑问题。对外部 JSON 同样应限制大小、深度并验证字段。
使用 shelve 保存简单键值数据
shelve 提供类似字典的持久化接口,值通常通过 pickle 保存:
import shelve
with shelve.open("settings") as database:
database["theme"] = "dark"
database["recent_files"] = ["notes.txt", "report.csv"]
with shelve.open("settings") as database:
print(database["theme"])
使用时要注意:
- 它继承 pickle 的不可信数据安全风险;
- 底层可能生成一个或多个文件,格式依赖运行平台;
- 不适合作为跨语言交换格式;
- 多进程并发读写可能损坏数据,不能把它当作完整数据库;
- 直接修改取出的可变对象不一定自动保存,应重新赋值,或了解
writeback=True的内存与关闭开销。
with shelve.open("settings") as database:
recent_files = database["recent_files"]
recent_files.append("todo.txt")
database["recent_files"] = recent_files
如何选择序列化格式
| 需求 | 推荐选择 |
|---|---|
| Web API、配置、跨语言交换 | JSON |
| 人工查看和版本控制 | 格式化 JSON |
| 大量独立、逐行处理的记录 | JSON Lines |
| Python 内部短期缓存复杂对象 | pickle,仅限可信数据 |
| 小型 Python 本地键值存储 | shelve,仅限可信环境 |
| 复杂查询、并发和可靠事务 | 数据库 |
选择前先回答四个问题:
- 数据是否来自可信来源?
- 是否需要被其他语言读取?
- 是否要跨 Python 或应用版本长期保存?
- 是否需要模式校验、并发、事务或查询?
如果答案涉及外部输入、跨语言或长期兼容,通常优先选择结构清晰且能显式校验的格式,而不是直接保存运行时对象。
数据版本与兼容性
长期保存的数据应包含格式版本,而不是假设代码结构永远不变:
import json
document = {
"schema_version": 1,
"user": {
"name": "小林",
"age": 20,
},
}
text = json.dumps(document, ensure_ascii=False)
读取时根据版本验证或迁移:
data = json.loads(text)
if data.get("schema_version") != 1:
raise ValueError("不支持的数据版本")
版本字段只能标识格式,仍然需要字段级验证。修改字段名称、类型或语义时,应定义升级路径并保留测试样例。
常见错误与最佳实践
把 JSON 当作任意 Python 对象的无损副本
JSON 的类型集合比 Python 少,元组、整数键和自定义对象不能自动无损往返。应设计明确的数据结构并测试序列化前后的语义。
混淆 dump 与 dumps
dump()写入文件对象;dumps()返回字符串;load()从文件对象读取;loads()从字符串、字节或字节数组读取。
忽略编码
JSON 在 Python 中通常先表现为 str。写文件时明确使用 UTF-8;传输字节时按协议进行编码和解码。
反序列化不可信 pickle
这不是一般格式错误,而是任意代码执行风险。外部数据不要使用 pickle。
没有数据版本
只要数据会保存到下一次发布之后,就可能遇到代码和数据结构变化。关键持久化格式应包含版本,并准备验证和迁移策略。
小结
- 序列化负责把对象转换为可保存或传输的数据,编码和压缩是不同步骤;
- JSON 跨语言、可读,适合接口、配置和长期交换,但只支持有限的数据类型;
- 使用
ensure_ascii=False保留可读中文,外部数据解析后还要校验结构和范围; - 多条流式记录可以使用 JSON Lines,不能简单连续写入多个 JSON 值;
- pickle 能保存更多 Python 对象,但兼容性依赖 Python 和代码结构;
- 永远不要反序列化不可信 pickle;需要可靠查询、并发或事务时应使用数据库。