4. 二进制格式基础与NumPy数组持久化
在科学计算中,文本格式(如CSV)虽然便于人眼阅读,但存在精度损失、体积膨胀和解析缓慢的问题。当数组规模达到百万级元素时,二进制持久化几乎是唯一务实的选择。NumPy 提供了两种原生二进制容器:单数组的 .npy 与多数组打包的 .npz。理解它们的字节级结构,是后续与 C/Fortran 程序、原始二进制流互操作的前提。
一、.npy 文件的物理结构
.npy 文件由三部分组成:一个 6 字节的魔数 \x93NUMPY、2 字节的版本号(主版本+次版本),随后是一个 2 或 4 字节的头部长度字段,最后是头部字典与数组数据本体。头部字典是一个 Python 字面量形式的字符串,描述数组的 dtype、形状(shape)、Fortran 顺序标志(fortran_order)等元信息。读取时,解析器先校验魔数,再按头部长度定位数据起始偏移,之后便可直接按 dtype 解释后续字节。
关键点在于:数据本体是连续的原始字节,没有任何分隔符或压缩。这意味着 .npy 的载荷部分与“原始二进制文件”在字节层面完全同构,差别仅在于前面多了一段自描述头部。这一性质正是互转的理论基础。
二、字节序与内存布局
字节序(endianness)决定多字节数值在内存中的排列方向。NumPy 的 dtype 字符串中,< 表示小端(低位字节在前),> 表示大端,= 表示本机序。例如 int32 值 1 在小端下字节为 01 00 00 00,大端下为 00 00 00 01。跨平台交换数据时,若两端字节序不一致而不做转换,读出的数值将完全错误——这是二进制互操作最常见的故障源。
内存布局则涉及 C 顺序(行优先)与 Fortran 顺序(列优先)。对于二维数组 A[i, j],C 顺序下相邻元素沿最后一维连续,Fortran 顺序下沿第一维连续。NumPy 默认使用 C 顺序,但通过 np.asfortranarray 或 order='F' 可切换。布局不一致时,即使字节序正确,按错误顺序解释也会得到转置般的错乱结果。因此互转前必须显式确认 dtype.byteorder 与 flags['C_CONTIGUOUS']。
三、.npz 的多数组打包
.npz 本质是一个 ZIP 归档,内部每个成员都是一个独立的 .npy 文件,成员名即数组键名。使用 np.savez 保存多个数组,np.load 读取时返回一个类似字典的对象,按需惰性加载各成员。由于 ZIP 支持 DEFLATE 压缩,np.savez_compressed 可显著减小体积,代价是读写时多一次解压开销。对于含大量重复值或稀疏结构的科学数据,压缩收益往往可观;而对已接近随机分布的浮点数据,压缩率有限。
四、与原始二进制互转的实现
从 NumPy 数组写出原始二进制,核心是 tofile 或 tobytes。前者直接写入文件,后者返回字节串。写入前必须确保数组是连续且字节序明确的:
import numpy as np
a = np.arange(6, dtype='<i4').reshape(2, 3) # 小端 int32
a.astype('<i4').tofile('data.bin') # 写出原始字节
# 读回时必须显式提供 dtype 与 shape
b = np.fromfile('data.bin', dtype='<i4').reshape(2, 3)
assert np.array_equal(a, b)
反向操作中,np.fromfile 只依据 dtype 解释字节流,形状信息完全丢失,必须由调用方按约定 reshape。这正是原始二进制“轻量但无自描述”的代价。若数据来自大端机器,应写成 dtype='>i4',或读入后用 byteswap() 转换。
与 .npy 互转时,可借助 np.load 读出数组再 tofile;反之,用 fromfile 重建数组后 np.save。若需保留 Fortran 布局,应在保存时传 order='F',并在读取端用相同顺序解释。
五、验证策略
互转是否成功,不能只看“文件生成了”。可靠的验证包含三层:其一,字节级比对,对同一数组分别经 .npy 载荷与 tofile 写出的文件做逐字节比较;其二,数值回读比对,用 np.array_equal 或对浮点用 np.allclose 并设定合理容差;其三,元信息核对,检查 dtype、shape、flags['C_CONTIGUOUS'] 与 dtype.byteorder 是否符合预期。对跨平台场景,还应在一端故意以错误字节序读取,确认能得到可识别的异常数值,从而证明字节序处理逻辑确实生效。
.bin 无自描述的缺陷,同时保留其零开销优势。综上,.npy 是“自描述头部 + 原始载荷”的组合,.npz 是多个 .npy 的 ZIP 封装;原始二进制则是剥离头部后的纯载荷。三者互转的技术关键始终落在字节序与内存布局这两个维度上,而验证则需同时覆盖字节、数值与元信息三个层面。