pickle保存NumPy数组体积膨胀的根本原因是其低版本协议将数组元素逐个转为ASCII字符串编码,如'F1.234567e+00',导致体积翻数倍;而np.save直接写二进制格式,保留原始dtype和shape,体积仅为其1/2到1/5。

为什么 pickle 保存 NumPy 数组会膨胀得离谱?
根本原因不是 NumPy 本身,而是 pickle 默认用 protocol 0(ASCII 文本)或低版本协议序列化数组时,会把整个 ndarray 的内存布局拆成 Python 对象逐个编码——尤其是当数组含大量小整数或浮点数时,每个元素都变成类似 'F1.234567e+00' 这样的字符串,体积直接翻几倍甚至十几倍。
更糟的是,如果数组是 object 类型(比如存了函数、类实例),pickle 会递归序列化所有引用对象,体积和耗时完全失控。
- 检查当前协议:运行
pickle.HIGHEST_PROTOCOL,Python 3.8+ 默认是 5,但如果你手动用了protocol=0或没传参数(旧版本默认 3),就踩坑了 -
np.save()/np.load()比pickle快 3–10 倍,体积通常只有 1/2 到 1/5 - 如果必须用
pickle(比如要和其他非 NumPy 数据一起打包),至少升到 protocol 5 并禁用 ASCII 模式
用 np.save 替代 pickle.dump 的实操要点
np.save 直接写二进制格式(`.npy`),保留原始 dtype 和 shape,不经过 Python 对象层,自然轻量又快。但它只处理单个数组——这点常被忽略。
- 单数组:直接
np.save('data.npy', arr),加载用np.load('data.npy') - 多数组:用
np.savez打包成 `.npz`(实际是 ZIP 归档),支持压缩:np.savez_compressed('bundle.npz', a=arr1, b=arr2) - 注意:
np.load('bundle.npz')返回的是numpy.lib.npyio.NpzFile对象,要取数据得写loaded['a'],不是直接返回 dict - dtype 为
object的数组不能用np.save—— 它会报ValueError: Object arrays are not supported
必须用 pickle 时怎么压体积?
典型场景是保存带元数据的结构体,比如 {'config': {...}, 'weights': np.ndarray, 'history': [...]}。这时不能全盘抛弃 pickle,但可以分层优化。
立即学习“Python免费学习笔记(深入)”;
- 把大数组提前用
np.save单独存成文件,再在 pickle 字典里只存路径或文件名 - 用
protocol=pickle.HIGHEST_PROTOCOL(Python 3.8+ 是 5),并显式指定:pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL) - 对 float32/float64 数组,考虑降精度:
arr.astype(np.float32)再存,体积减半且多数场景无感 - 避免
np.array([...], dtype=object)—— 改用list或结构化 dtype(如np.dtype([('name', 'U20'), ('score', 'f4')]))
joblib 是不是万能解药?
joblib 确实针对 NumPy 做了深度优化,默认用 np.save 存数组、pickle 存其余部分,还支持内存映射和压缩。但它不是“开箱即用就最小”的。
- 默认不压缩:加
compress=3(整数 1–9)才启用 zlib 压缩,体积可再降 30%–60% - 慎用
mmap_mode:虽然节省内存,但加载时会变慢,且文件不能被移动或删除 - 跨 Python 版本兼容性差:joblib 1.3 保存的文件,可能在 1.4 加载失败,不如纯
np.savez_compressed靠谱 - 它仍会 pickle 非数组部分,如果字典里混了大量小字符串或嵌套对象,体积还是比纯 NumPy 方案大
真正省事又小的方案,永远是:数组走 np.savez_compressed,其他数据另存 JSON 或 msgpack —— 不硬塞进一个序列化器里。


















