Python · 序列化: pickle / JSON / msgpack

对象与字节的三条通道: pickle 深而危险, JSON 通用而粗糙, msgpack/orjson 快而紧凑; 边界外永远是不可信输入

pickle.dumps → bytes json.dumps → str packb → bytes Order 对象 (内存) id = UUID(...) · total = Decimal created_at = datetime items = tuple[Item, ...] 发送前必须变成字节/文本 pickle — 二进制深序列化 protocol 5 (PEP 574) · Python 专用 保类型: datetime/Decimal/自定义类原样复活 对象图/重复引用/递归结构都保得住 __reduce__ 可携带任意调用 unpickle 外部数据 = 远程代码执行 不加密 · 不签名 · 完整性无保障 类改名/挪路径后旧档全炸 只在自己可信通道里用 (本机/自有存储) ml 模型惯例: joblib.dump/load JSON — 文本 · 跨语言 只有 7 种: dict/list/str/num/bool/None 任何语言都能读 · 可审计可 diff 类型磨损区 datetime → str (格式自己定) tuple → list (不可变性丢失) int key → str key · Decimal→float 漂移 扩展点: dumps(default=) 出口 loads(object_hook=) 进口 msgpack — 二进制紧凑 类型比 JSON 丰富: bin/ext/int 分宽 同样数据比 JSON 小 30-50% 中文无 \u 转义膨胀 · 无缩进歧义 str/bytes 语义严格区分 (use_bin_type) 仍是"裸格式": schema 靠 pydantic 补 MQ 消息体 · Redis 缓存 · 带宽敏感链路 同族提速项: orjson (Rust 实现) dumps 快 json 5-10 倍, 原生吃 datetime/UUID/dataclass, 出 bytes 铁律边界: 界外不可信, 界内才可信 外部输入 请求体 / MQ / 缓存 / 文件 可能缺字段/多字段/被篡改 pydantic 校验 类型/默认值/未知字段策略 model_validate_json 一步到位 内部类型化对象 不变式成立, 业务放心用 pickle 只活在界内 界内跨版本也要版本号: 缓存 key 带 schema 版本, 数据文件演进靠 __setstate__ 迁移 Legend 内存对象 pickle / 危险 JSON 文本通道 msgpack / 安全输出 边界 / 磨损警示

pickle: 深而危险

  • • 保类型保对象图, Python 对面 Python 几乎无损
  • • unpickle 会执行 __reduce__ 里的调用 —— 外部输入 = RCE
  • • 不加密不签名, 类路径一改旧档全炸

JSON: 通用而粗糙

  • • 7 种基本类型, 换来任何语言都能读
  • • datetime/tuple/int-key 全要磨损, 两岸自己约定
  • • default/object_hook 是唯一的扩展点

msgpack/orjson: 快而紧凑

  • • 二进制小 30-50%, 中文没有 \u 膨胀
  • • orjson 快 5-10 倍还原生吃 datetime/UUID
  • • 快不等于安全: schema 还得 pydantic 把关

💡 一句话理解

序列化就是把内存对象压成可传输的字节/文本, 三条通道各换一样东西: pickle 用安全性换"深" —— 类型、对象图、重复引用全保住, 但 unpickle 会执行字节流里携带的调用; JSON 用表达力换"通用" —— 只有 7 种类型, datetime 变 str、tuple 变 list, 却让任何语言都能读; msgpack/orjson 用可读性换"快与紧凑"。选型第一问永远是"对面是谁、可不可信": 外部边界上的数据一律先过 pydantic 校验再进内部对象。

🧠 必知必会 必考 & 必会

pickle 本质
把对象图递归拆成 opcode 流(protocol 0-5), loads 时按指令重建; 二进制、Python 专用、跨版本有兼容风险。
import pickle
b = pickle.dumps({"ts": datetime.now()}, protocol=5)
b[:1]                       # → b'\x80' = PROTOCOL opcode 头
pickle.loads(b)             # → datetime 原样复活
# 关键: 二进制 opcode 流, Python 专用
__reduce__ 系列
__reduce__ 定义"重建时要调谁、传什么参"(正是 RCE 入口); __getstate__/__setstate__ 自定义快照与恢复 —— 版本迁移的标准钩子。
class Cfg:
    def __reduce__(self):            # 重建 = Cfg(self.dsn)
        return (Cfg, (self.dsn,))    # ← RCE 入口也在这
    def __getstate__(self):
        return {"dsn": self.dsn}     # 自定义快照
    def __setstate__(self, s):       # 恢复 = 迁移钩子
        self.__dict__.update(s)
unpickle = 代码执行
loads 会执行 reduce 给出的 callable; 反序列化外部 pickle 等于远程执行任意代码(CVE 高发区), 永不信任外部 pickle。
class Evil:
    def __reduce__(self):          # 字节流携带"重建指令"
        return (os.system, ("rm -rf /tmp/x",))
blob = pickle.dumps(Evil())
pickle.loads(blob)             # → 真的执行了 os.system
# 关键: loads 会执行 reduce 给出的 callable
joblib/ml 惯例
sklearn 管道用 joblib.dump/load: pickle 语义 + numpy 大数组分块压缩; 模型文件要和代码版本成对管理。
pipe = Pipeline([( "scaler", StandardScaler()),
                 ("clf", LGBMClassifier())]).fit(X, y)
joblib.dump(pipe, "m/churn_v7.joblib", compress="lz4")
pipe = joblib.load("m/churn_v7.joblib")  # 整体原子恢复
# 关键: 模型文件与 sklearn 版本成对管理
json 类型映射
dict→object, list/tuple→array, str→string, int/float→number, True/False→true/false, None→null; 其余类型默认 TypeError。
json.dumps({"ok": True, "n": None, "t": (1, 2)})
# → '{"ok": true, "n": null, "t": [1, 2]}'
json.dumps({"ts": datetime.now()})
# → TypeError: Object of type datetime is not ...
# 关键: 只有 7 种基本类型, 其余默认炸
default / object_hook
dumps(default=fn) 接住出口不认识的类型; loads(object_hook=fn) 在进口重建自定义类型 —— 两个官方扩展点, 别满世界找"第三个"。
def default(o):                       # 出口: 接住不认识的
    if isinstance(o, datetime): return o.isoformat()
    raise TypeError(type(o))
def hook(d):                          # 进口: 重建自定义类型
    if "ts" in d: d["ts"] = parse(d["ts"])
    return d
json.loads(s, object_hook=hook)       # 两个官方扩展点
orjson
Rust 实现, dumps/loads 快 5-10 倍, 原生支持 datetime/UUID/dataclass/numpy(需 opt), 输出 bytes; 不支持 indent, 美化用 opt=OPT_INDENT_2。
import orjson
orjson.dumps({"ts": datetime.now(), "id": uuid4()})
                              # → bytes: 原生吃 datetime/UUID
orjson.dumps(x, option=orjson.OPT_INDENT_2)  # 美化
# 关键: 无 indent 参数, 输出是 bytes
msgpack
二进制 JSON 同构格式: int 按宽度编码、真二进制 bin、ext 扩展; packb/unpackb, MQ 与缓存的降带宽首选。
import msgpack
b = msgpack.packb({"中文": [1, 2]}, use_bin_type=True)
b                      # → 二进制, 无 \u 转义膨胀
msgpack.unpackb(b, raw=False)  # → {'中文': [1, 2]}
# 关键: int 按宽度编码, 比 JSON 小 30-50%
pydantic 一站式
model_validate_json(进口校验) + model_dump_json(出口白名单序列化) 一步到位; model_config 管未知字段策略。
class Req(BaseModel):
    model_config = ConfigDict(extra="forbid")  # 未知字段策略
    uid: int
    ts: datetime
req = Req.model_validate_json(body)   # 进口: 校验+解析一步
out = UserView.model_dump_json()      # 出口: 白名单序列化
版本兼容
新增字段必须带默认值; 未知字段定好 ignore/forbid; pickle 旧档升级用 __setstate__ 做字段迁移; 缓存 key 带 schema 版本号。
class Profile:
    def __setstate__(self, s):
        if "region" not in s:        # 旧档迁移: 补默认值
            s["region"] = "cn"
        self.__dict__.update(s)
key = f"profile:v3:{uid}"         # 关键: 结构变 → bump v4
ensure_ascii / separators
默认 ensure_ascii=True 中文全变 \uXXXX 体积膨胀数倍; separators=(",", ":") 去掉多余空格再省 10-15%。
len(json.dumps("支付"))                      # → 14 (两个 \uXXXX)
len(json.dumps("支付", ensure_ascii=False))  # → 4
json.dumps(d, ensure_ascii=False,
           separators=(",", ":"))    # 再省 10-15% 空格
parquet/feather 一句话
大 DataFrame 落盘用列存 parquet(压缩+跨语言), 中间缓存用 feather(mmap 秒读) —— 别拿 pickle 扛数据管道。
df.to_parquet("features/v7.parquet",
              compression="zstd")       # 列存压缩, 跨语言
df = pd.read_parquet(p, columns=["f1"])  # 按需只读部分列
df.to_feather("tmp/scratch.feather")  # mmap 秒读中间缓存
# 关键: 别拿 pickle 扛数据管道

🏭 生产实战 real world

场景 1 · ml 模型 + 预处理管道: joblib 整体落盘

模型上线要保证"标准化器 + 特征工程 + 分类器"作为一个整体原子保存, 单独存权重会版本错配:

import joblib
from sklearn.pipeline import Pipeline

pipe = Pipeline([
    ("scaler", StandardScaler()),            # 预处理与模型同进同出
    ("clf",    GradientBoostingClassifier()),
]).fit(X_train, y_train)

joblib.dump(pipe, "models/churn_v7.joblib", compress="lz4")
# 服务端加载: 与训练时同版本 sklearn, 否则 unpickle 可能炸
pipe = joblib.load("models/churn_v7.joblib")
prob = pipe.predict_proba(feats)[0, 1]

模型文件名带版本 + 元数据登记 sklearn 版本: 线上模型与训练环境可追溯、可回滚。

场景 2 · Redis 缓存层 pickle 存对象, 版本号 key 防结构漂移

用户画像对象每请求重建要 300ms, 想整对象缓存; 但直接 pickle 裸 key 上线, 改一次类定义缓存就静默错乱:

import pickle, redis

r = redis.Redis()
SCHEMA = "profile:v3"                # 类结构一变就 bump: v3→v4

def get_profile(uid: int) -> Profile:
    key = f"{SCHEMA}:{uid}"
    if raw := r.get(key):                  # Redis 出入都是 bytes, 正配 pickle
        return pickle.loads(raw)          # 来源=自己写入, 可信通道
    p = build_profile(uid)                 # 300ms 的重建
    r.setex(key, 3600, pickle.dumps(p, protocol=5))
    return p
# 事故教训: 不带版本号时, 旧 schema 的缓存命中后字段缺失, 线上 nil 错乱无感知

场景 3 · API 响应 orjson 替换 json: 列表接口提速

商品列表 4MB JSON 响应, 序列化占 80ms; 换 orjson 的 ORJSONResponse 三行收工:

from fastapi.responses import ORJSONResponse
from fastapi import FastAPI

app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/skus", response_class=ORJSONResponse)
def list_skus():
    return {"items": sku_repo.all()}   # datetime/UUID 直接吃, 不用 default

# 压测: 4MB 响应序列化 82ms → 11ms; P99 340ms → 265ms
# 注意: ORJSONResponse 输出紧凑无缩进, 调试期想看格式再手动 orjson.dumps(..., option=OPT_INDENT_2)

场景 4 · 自定义 JSON 编码器: datetime/Decimal/UUID 统一口径

接口里日期格式有的带 Z 有的不带、金额直接 float 漂移一分钱; 出口统一走一个 default:

import json
from datetime import datetime, timezone
from decimal import Decimal
from uuid import UUID

def json_default(o):
    if isinstance(o, datetime):
        return o.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
    if isinstance(o, Decimal):
        return str(o)                      # 金额走字符串, 精度零漂移
    if isinstance(o, UUID):
        return str(o)
    raise TypeError(f"not serializable: {type(o)}")  # 别静默吞

out = json.dumps(order, default=json_default, ensure_ascii=False)

场景 5 · 对外接口 pydantic 白名单: 内部字段不得出门

User ORM 对象直接 json 化返回, 把 password_hash/is_internal 字段一起漏了出去; 出口模型只放白名单:

from pydantic import BaseModel, ConfigDict

class UserPublic(BaseModel):
    model_config = ConfigDict(extra="forbid")    # 多余字段直接报错
    uid: int
    name: str
    created_at: datetime

def user_view(u: UserORM) -> str:
    # 从 ORM 挑选字段构造 → password_hash 之类根本进不来
    return UserPublic.model_validate(u, from_attributes=True).model_dump_json()

# 内部对象随便长, 出口只认视图模型: "序列化面"即攻击面

场景 6 · MQ 消息体 msgpack: 二进制降带宽

行情推送 5 万条/秒, JSON 文本 1.8KB/条把带宽打满; 换 msgpack:

import msgpack

def publish(quote: dict):
    body = msgpack.packb(quote, use_bin_type=True)   # str/bytes 严格区分
    mq.send(routing_key="quote", body=body, content_type="application/msgpack")

def consume(body: bytes):
    return msgpack.unpackb(body, raw=False)      # raw=False: str 按 str 还原

# 实测: 平均 1.8KB → 0.9KB, 网卡出流量减半;
# 嵌套数字数组 (行情快照) 收益最大, 比 JSON 小 50%+

场景 7 · 配置快照用 JSON 不用 pickle

风控参数要留档审计、diff 出变更单; pickle 是黑盒字节, JSON 一眼可读:

def snapshot(cfg: RiskConfig, path):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(cfg.model_dump(), f, ensure_ascii=False,
                  indent=2, sort_keys=True)   # 排序=稳定 diff

# git diff 两个快照: 参数谁改了、何时改的一目了然
# pickle 快照: 无法 diff、无法 review、还绑着类路径 —— 审计场景禁用

场景 8 · 跨版本数据文件演进: __setstate__ 迁移旧字段

训练产物 FeatureStore v1 落盘的 pickle, 类升级到 v3 后 loads 全炸; 在恢复钩子里做迁移:

class FeatureStore:
    VERSION = 3
    def __getstate__(self):
        return {"v": self.VERSION, "feats": self.feats, "norm": self.norm}
    def __setstate__(self, state):
        v = state.get("v", 1)                    # 旧档没有版本字段 = v1
        if v < 2: state["norm"] = default_norm()   # v1→v2: 补归一化参数
        if v < 3: state["feats"] = migrate_keys(state["feats"])
        self.__dict__.update(state)           # 迁移完再落到实例上

旧档新代码照样读; 配合"读时迁移 + 写时升版", 数据文件可以横跨数个版本存活。

场景 9 · 可信通道补充: pickle + HMAC 签名防篡改

任务队列要传复杂对象(不跨语言), pickle 最省事但怕中间被改; HMAC 给它上封条(注意: 这是防篡改, 不是加密):

import pickle, hmac, hashlib
from app.config import settings            # KEY 走 K8s Secret 注入

KEY = settings.JOB_HMAC_KEY                  # 双端共享, 绝不入库入仓

def seal(obj) -> bytes:
    body = pickle.dumps(obj, protocol=5)
    sig = hmac.new(KEY, body, hashlib.sha256).digest()
    return sig + body                       # 前 32 字节 = 签名

def unseal(blob: bytes):
    sig, body = blob[:32], blob[32:]
    expect = hmac.new(KEY, body, hashlib.sha256).digest()
    if not hmac.compare_digest(sig, expect):   # 恒时比较, 防时序侧信道
        raise TamperedJobError("signature mismatch")
    return pickle.loads(body)             # 验签通过才 unpickle

场景 10 · 大 DataFrame 选型: parquet 落盘, feather 中转

特征工程 200 万行 × 380 列, 有人用 pickle 整存: 文件 1.9GB、读回 40s、只能 Python 读; 换列存:

df.to_parquet("features/v7.parquet", compression="zstd")
# 1.9GB → 420MB (zstd 列压缩), 读回 3.2s, 按需只读部分列 (columns=[...])
# 跨语言: Spark/Arrow/Rust 都能读 —— 数据不再绑死 Python

df.to_feather("tmp/scratch.feather")
# 流水线中间缓存: mmap 零拷贝, 下游进程秒级拿到同一份内存
# 一句话: 归档/共享 parquet, 短命中中间态 feather, pickle 不进数据管道

⚠️ 编码注意与常见坑 pitfalls

坑 1 · unpickle 外部数据 = 远程代码执行 — 上传接口拿用户文件直接 pickle.loads, 攻击者构造 __reduce__ 为 os.system 的 payload 即可 getshell。原因: loads 会执行字节流携带的调用。正解: 外部输入只收 JSON/msgpack + pydantic; pickle 永不接触不可信数据。
blob = request.data            # 用户上传的文件
pickle.loads(blob)             # 错: blob 的 __reduce__ 是
                               #     os.system → 直接 getshell
data = json.loads(blob)        # 对: 外部输入只收 JSON/msgpack
Req.model_validate(data)       #     + pydantic 校验
坑 2 · 高协议版本低版本 Python 读不了 — 3.12 上 protocol=5 存的档, 3.7 环境加载报 unsupported pickle protocol。原因: 协议 4/5 需要 3.4+/3.8+。正解: 跨版本通道显式降 protocol=4; 部署环境矩阵写进发布检查单。
pickle.dumps(obj, protocol=5)   # 3.12 存档
# 3.7 加载 → ValueError: unsupported pickle protocol: 5
pickle.dumps(obj, protocol=4)   # 对: 跨版本显式降协议
# 部署环境矩阵写进发布检查单
坑 3 · 类改名/挪路径后旧 pickle 全炸 — 重构把 app.models.Order 挪到 app.core.order, 半年后 loads 历史缓存抛 AttributeError: Can't get attribute。原因: pickle 按模块路径找类。正解: 老 sys.modules 别名兼容, 或缓存清空 + schema 版本号强制失效。
# 重构: app.models.Order → app.core.order.Order
pickle.loads(cache)             # 错: Can't get attribute 'Order'
                                #     on <module 'app.models'>
sys.modules["app.models"] = shim   # 对: 旧路径别名兼容
# 或: bump schema 版本号 → 缓存整体失效
坑 4 · json 序列化 datetime 直接 TypeError — json.dumps(order) 在第一个 datetime 上就炸 Object of type datetime is not JSON serializable。原因: 标准库只认 7 种类型。正解: default=str 应急, 生产用统一 default 函数(场景 4)或直接 orjson。
json.dumps({"ts": datetime.now()})
# 错: TypeError: Object of type datetime
#     is not JSON serializable
json.dumps({"ts": ts}, default=str)   # 应急
orjson.dumps({"ts": ts})             # 对: 原生支持
坑 5 · Decimal 转 float 精度漂移 — 金额 Decimal("99.10") 经 float 变 99.09999999999999, 对账差一分。原因: 二进制浮点无法精确表示十进制小数。正解: 金额序列化一律 str(Decimal), 两岸约定字符串承载; JS 侧用 decimal 库。
json.dumps({"amt": float(Decimal("99.10"))})
# 错: → 99.1 — 尾零丢失, 大额/累加漂移差一分
json.dumps({"amt": Decimal("99.10")}, default=str)
# 对: → "99.10" 字符串承载, 两岸约定
坑 6 · tuple 序列化成 list — 发出去的坐标 (1, 2), 对面 json.loads 回来是 [1, 2], 不可变性丢失, 当 set 元素/字典 key 直接 TypeError。原因: JSON 没有元组概念。正解: 两岸约定"数组即 list"; 需要元组语义就在 object_hook 里重建。
json.dumps({"pt": (1, 2)})     # → '{"pt": [1, 2]}'
pt = json.loads(s)["pt"]       # → [1, 2] 是 list!
{pt: "x"}                      # 错: TypeError: unhashable type
tuple(pt)                      # 对: 读取侧重建元组语义
坑 7 · dict int key 变 str key — {1: "a"} dumps 成 {"1": "a"}, loads 回来 d[1] 抛 KeyError。原因: JSON 对象 key 只能是字符串。正解: 读取侧统一 int(k) 转换; 或 key 本来就该用字符串设计。
json.dumps({1: "a"})           # → '{"1": "a"}'
d = json.loads('{"1": "a"}')
d[1]                          # 错: KeyError: 1
d[int("1")]                    # 对: 读取侧 int(k) 转换
坑 8 · nan/Infinity 是非标准 JSON — json.dumps(float("nan")) 默认输出字面量 NaN, 前端 JSON.parse 直接 SyntaxError。原因: Python 默认 allow_nan=True 是私货扩展。正解: 出口前 math.isnan 清洗成 null; 或 allow_nan=False 让它及早抛错。
json.dumps({"v": float("nan")})  # → '{"v": NaN}' 非标准!
JSON.parse('{"v": NaN}')         # 错: JS 端 SyntaxError
json.dumps(x, allow_nan=False)    # 对: 及早抛 ValueError
# 或出口前 math.isnan 清洗成 null
坑 9 · ensure_ascii=True 中文全 \u 转义 — 中文消息序列化成 "\u652f\u4ed8", 体积膨胀 3-6 倍, 日志/检索不可读。原因: 标准库默认转 ASCII。正解: ensure_ascii=False + UTF-8 编码写文件; orjson 默认就不转。
json.dumps({"msg": "支付"})
# 错: → '{"msg": "\u652f\u4ed8"}' 膨胀 3-6 倍
json.dumps({"msg": "支付"}, ensure_ascii=False)
# 对: → '{"msg": "支付"}'; orjson 默认就不转
坑 10 · orjson 不支持 indent 等行为差异 — 从 json 迁到 orjson, dumps(x, indent=2) 直接 TypeError; 个别类型(numpy)要 opt。原因: orjson 输出 bytes、参数模型完全不同。正解: 缩进用 orjson.OPT_INDENT_2; 迁移时按官方支持矩阵过一遍类型。
orjson.dumps(x, indent=2)      # 错: TypeError: Unexpected
                               #     keyword argument 'indent'
orjson.dumps(x, option=orjson.OPT_INDENT_2)  # 对: 缩进
orjson.dumps(x).decode()       #     输出是 bytes 要 decode
坑 11 · 循环引用 json 死递归爆栈 — a.b = b, b.a = a, json.dumps(a) 抛 RecursionError: maximum recursion depth exceeded。原因: json 不记录已访问对象。正解: 关系建模改存外键 id; 或先 copy.deepcopy 断环/pickle(它支持循环引用)。
a["b"] = b; b["a"] = a
json.dumps(a)     # 错: RecursionError: maximum
                  #     recursion depth exceeded
a["b_id"] = b["id"]; del a["b"]  # 对: 改存外键 id
# 或直接 pickle — 它支持循环引用
坑 12 · 大对象 pickle 内存峰值双份 — 2GB DataFrame pickle.dumps 瞬间进程 RSS 冲到 4GB+, 容器 OOMKill。原因: 原对象 + 完整字节串同时在内存。正解: 用 pickle.dump(df, file) 流式写文件(joblib 分块更好); 别 dumps 到内存再写。
blob = pickle.dumps(df_2gb)    # 错: 原对象+字节串双份
                               #     → RSS 4GB+, OOMKill
with open(p, "wb") as f:       # 对: 流式写文件
    pickle.dump(df_2gb, f)
joblib.dump(df, p, compress="lz4")  #     分块更好
坑 13 · pickle 不加密也没签名 — 以为存成 pickle 别人就改不了, 实际字节随手可改, loads 照单全收。原因: pickle 只是格式, 无任何完整性机制。正解: 可信存储/传输通道 + HMAC 签封(场景 9); 敏感数据加密走 cryptography 库。
blob = pickle.dumps(job)
blob = blob[:-1] + b"\x41"     # 错: 字节随手可改,
pickle.loads(blob)             #     loads 照单全收
sig = hmac.new(KEY, body, sha256).digest()  # 对: HMAC 签封
# 验签通过才 loads (场景 9)
坑 14 · dataclass 版本演进默认值坑 — 新版给 @dataclass 加字段没给默认值, 旧 JSON 反序列化全抛 missing argument。原因: 构造函数签名变严。正解: 新增字段一律 field(default_factory=...); pydantic 侧配 extra 策略和默认值。
@dataclass
class OrderV2:
    uid: int
    coupon: str          # 错: 新字段无默认值, 旧档全抛
@dataclass                  #     TypeError: missing argument
class OrderV2:
    uid: int
    coupon: str | None = None   # 对: 一律带默认值
坑 15 · 序列化闭包/连接对象直接失败 — 对象里挂着 Session/Engine/lambda, pickle.dumps 抛 TypeError: cannot pickle。原因: 绑定运行时状态的对象无法字节化。正解: __getstate__ 剔除不可序列化成员, __setstate__ 里重建连接。
class Svc:
    session: Session        # 挂着连接/lambda
pickle.dumps(Svc())         # 错: TypeError: cannot pickle
def __getstate__(self):
    s = self.__dict__.copy()
    s.pop("session")       # 对: 剔除后快照,
    return s                #     __setstate__ 里重建连接
坑 16 · msgpack 与 json 混用 bytes/str 混淆 — unpackb(b"...") 出来的 key 全是 bytes(b"user"), d["user"] KeyError; 或忘了 use_bin_type 把 str 存成 bin。原因: msgpack 区分 str/bin, 默认行为随版本变过。正解: 固定 packb(..., use_bin_type=True) + unpackb(..., raw=False, strict_map_key=False) 并写进公共封装。
d = msgpack.unpackb(body)   # 错: key 全是 bytes,
d["user"]                   #     → KeyError (要 d[b"user"])
d = msgpack.unpackb(body, raw=False,
                    strict_map_key=False)  # 对: str 按 str 还原
msgpack.packb(x, use_bin_type=True)   # 固定进公共封装
坑 17 · repr 当序列化存库 — 有人 str(obj)/repr(obj) 存库, 恢复时 ast.literal_eval 解析: 换行/引号/嵌套对象分分钟解析失败, 且完全无 schema。正解: 存结构化格式(JSON/msgpack); repr 只用于日志与调试展示。
db.save(str(obj))                  # 错: repr 换行/引号/嵌套,
ast.literal_eval(saved)            #     解析分分钟失败, 无 schema
db.save(json.dumps(obj))           # 对: 结构化格式存库
obj = json.loads(saved)            # repr 只用于日志展示
坑 18 · 对象图重复引用: pickle 保结构 json 拍平 — 同一个 Item 被 order.items 和 order.ref 共引用: pickle loads 回来还是同一个对象; json dumps 变成两份独立拷贝, 一处修改另一处不跟。原因: JSON 树模型无引用概念。正解: 需要引用语义选 pickle/自定 id 机制; JSON 场景按 id 再组装。
it = Item(1); order.items, order.ref = [it], it
o = pickle.loads(pickle.dumps(order))
o.items[0] is o.ref            # → True: pickle 保引用
j = json.loads(json.dumps(order, default=obj_dict))
j["items"][0] is j["ref"]      # 错: False — 独立拷贝
# 对: JSON 存 id, 读取侧按 id 再组装
坑 19 · 缓存 pickle 命中旧 schema 无感知 — 改了类字段发布后, 旧缓存命中返回老结构, 代码按新结构取字段拿到 None/AttributeError, 而且只在线上偶发(取决于 key 命中)。正解: 缓存 key 强制带 schema 版本号(场景 2), 结构变更 = bump 版本整体失效。
key = f"profile:{uid}"         # 错: 类字段改了, 旧缓存
p = pickle.loads(r.get(key))  #     命中 → 偶发 None/AttributeError
key = f"profile:v3:{uid}"      # 对: schema 版本号,
                               #     结构变 → bump v4 整体失效
坑 20 · pydantic dump 模式不分 — model_dump() 默认 python 模式, datetime 还是 datetime 对象; 直接塞给 json.dumps 又 TypeError。原因: mode="python" 与 mode="json" 输出类型不同。正解: 要 JSON 字节就用 model_dump_json(); 要 dict 给 json.dumps 就 model_dump(mode="json")。
u.model_dump()                # datetime 还是 datetime 对象
json.dumps(u.model_dump())    # 错: 又 TypeError
u.model_dump(mode="json")     # 对: 要 dict 给 json.dumps
u.model_dump_json()           # 要 JSON 字节直接用这个