首页 / 资讯中心 / 文章详情

NumPy数据存取全解析:从npy/npz到内存映射的工程实践

NumPy数据存取全解析:从npy/npz到内存映射的工程实践 ★ FEATURED ARTICLE
1. 为什么偏偏要单独讲一次数据存取1.1 你在哪些场景里被这件事绊倒过矩阵算完了程序一关内存一释放结果全没了。这是做数据处理的人都会撞上的问题。重跑一遍也不是不能忍可是十万行的表格、一次迭代很久的算法、一整套预处理好的特征矩阵每次都从零开始浪费的就不只是时间还会消磨耐心。我自己最早就是这样跑一个模拟项目时得到一组权重当时偷懒没存盘第二天改脚本时不小心清掉了终端只能重新跑两个多小时。从那以后凡是超过三十秒能算完的结果我第一反应就是先落盘。数据存储还有一层需求经常被忽略中间结果是要给后续脚本用的。数据清洗和模型训练经常拆成不同脚本前一阶段输出的特征矩阵后一阶段要读进来。如果每次都口头约定格式或者临时导出成稀奇古怪的文本很快就会被文件命名和分隔符问题折磨到怀疑人生。NumPy 内置的读写接口恰好是这一层最稳的兜底方案不需要安装额外包只靠几个核心函数就能覆盖绝大多数场景。1.2 二进制和文本到底差在哪初学者最常犯的误区是把数据保存简单理解成“写一个 CSV”。文本格式确实直观用记事本就能打开看但代价是性能损耗和精度丢失。一个 float64 数组用np.savetxt写成文本再读回来看起来数值差不多实际上每一列都要经过字符串解析、类型转换和边界判断速度能差好几个数量级。更麻烦的是文本文件里单纯记数字不会自动带上 shape 和 dtype 信息三维矩阵存成文本以后读回来往往只剩二维轮廓还得手动 reshape。二进制方案就不一样。.npy文件自带文件头里面写明了 shape、dtype、字节序这些元信息保存和加载时直接做内容搬运少了很多中间环节。这个区别平时不明显等你处理 1GB 量级的矩阵时就很痛文本方案不仅写得很慢读的时候还可能把内存短暂吃满。二进制方案几乎不会为读写消耗额外资源唯一要付的代价是磁盘空间和“给人类直接看”的可读性。换句话说机器读的用二进制人看的用文本两者不是替代关系而是分工关系。1.3 一个维持了大半个项目寿命的选择原则我自己定了一条原则计算型中间结果一律用.npy和.npz需要跨工具查看、给别人看图、或者要与表格软件交换的场景才导出 CSV 或 TXT。这样既保住了性能和元信息又留了一个“给人看”的出口。这个原则在团队协作里尤其有用因为文件格式一旦统一读写逻辑就变得非常齐整所有脚本的读取段都写成np.load输出段都写成np.save/np.savez不会出现“这段数据是从哪个表里哪一列拼出来的”这种悬案。值得多说一句不要把数据存储想成“写代码的最后一步”而要想成“数据集的一个版本”。每次实验产出一份.npz相当于给自己的成果做了快照后面出问题还能回头对比。这个习惯比任何存储技巧都更值钱。2. 核心函数与存储方式拆解2.1 np.save 和 np.load单数组的默认答案最基础的一组组合是np.save(文件名.npy, 数组)和np.load(文件名.npy)。保存时路径可以省略.npy后缀NumPy 会自动补上读取时建议把后缀写全省得它猜。这个接口最直观的好处是读回来以后 shape、dtype、甚至字节序都能原样还原你不需要额外记住自己当初存的是三维还是四维、是float32还是float64。import numpy as np feature np.arange(24, dtypenp.float32).reshape(2, 3, 4) np.save(feature.npy, feature) loaded np.load(feature.npy) print(loaded.shape) # (2, 3, 4) print(loaded.dtype) # float32很多人在这一步会踩到allow_pickle的坑。NumPy 较新的版本里np.load的allow_pickle默认是False如果文件本身内部带了 pickle 对象读取时就会直接报错 “ValueError: Object arrays cannot be loaded when allow_pickleFalse”。解决办法很简单要么在保存时保证数组是数值类型要么在明确知道文件来源安全的前提下传np.load(..., allow_pickleTrue)。我个人的习惯是“默认不加明确需要才加”这样做可以顺便提醒自己不要在.npy里混进不可靠的对象。2.2 np.savez 和 np.savez_compressed多个数组打包带走一个模型实验通常不止一个数组特征矩阵、标签、权重可能还有一组评估指标。如果每个数组都单独存一个.npy文件数量会快速膨胀来回拷项目时很容易漏掉其中一个。np.savez就是为这种情况设计的它能把任意多个数组按名字打包进一个.npz压缩包本质上是一个 ZIP 容器读的时候用np.load然后像访问字典一样取出各个数组。X np.random.rand(1000, 50) y np.random.randint(0, 2, size1000) w np.ones(50) np.savez(experiment_1.npz, XX, yy, weightsw) res np.load(experiment_1.npz) print(res.files) print(res[y].shape)res.files会给出里面的全部键名。有个细节容易忽略如果保存时没写关键词只用了位置参数np.savez(a.npz, arr1, arr2)自动分配的名字是arr_0、arr_1后面要逐一确认“哪个是哪个”非常容易被搞混。所以我在日常代码里几乎总是显式给数组起名宁可多敲几个字母也不要回到“arr_0 到底是哪个指标”的困惑里。np.savez_compressed在同样的接口下额外做了压缩。对于有大量重复值或可压缩模式的数据文件体积能缩小很多代价是保存和读取会消耗一点 CPU。如果你的矩阵里有很多 0 或者重复数值建议直接上np.savez_compressed如果数据本身接近随机浮点数压缩率通常不明显用普通np.savez就够。我一般在保存中间结果时用普通版在归档备份、往其他环境传演示数据时改用压缩版。2.3 np.savetxt / np.loadtxt / np.genfromtxt文本方案不能只会一种文本方案最大的价值是可视化。经常有人要求“把数据导出来我看看”这时候给一个.npy对方打不开给一个 CSV 反而大家都懂。np.savetxt可以设置分隔符、格式和跳行读取时np.loadtxt最常用。一个比较典型的写法是这样的arr np.linspace(0, 1, 100).reshape(10, 10) np.savetxt(sample.csv, arr, delimiter,, fmt%.6f) loaded np.loadtxt(sample.csv, delimiter,, skiprows0)fmt参数值得养成习惯去设置。如果不写NumPy 会按最高精度输出文件会非常大加载速度也受影响。像这种只给人看的结果fmt%.6f或fmt%.4f就足够了。要是文件里有表头np.loadtxt可以用skiprows1跳过去要是表头里还带着字符串列就要轮到np.genfromtxt出场了。np.genfromtxt比loadtxt更宽容它支持缺失值、字符串列、指定 dtype还能通过namesTrue把第一行当成字段名。处理调查问卷、传感器日志这类“不干净”的数据时它几乎是第一选择。虽然它的性能比loadtxt慢一点但在文件本身不大、又包含混合类型的场景里稳定性和灵活性更重要。data np.genfromtxt( log_with_header.csv, delimiter,, namesTrue, dtypeNone, encodingutf-8 ) print(data[temperature]) # 按表头字段名取列2.4 别忘了 allow_pickle 和 encoding 这两个隐藏选项前面提到allow_pickle还有一个容易被忽略的是encoding。np.load处理旧版本生成的.npy文件时如果文件头里含有非 ASCII 字符可能需要在读取时传入encodinglatin1或encodingbytes才能正常打开。这个情况多见于跨 Python 版本共享数据尤其是涉及旧版本生成的文件。解决办法不复杂先看报错提示的是哪一段名称再传入对应参数读取然后立刻用当前版本重新保存一份。处理完以后我习惯把源文件重命名加一个.backup后缀防止以后再次误读。保存方式适用场景主要优势主要代价np.save / np.load单个数组的中间结果自动保存 shape 和 dtype速度最快只支持单数组不可读np.savez / np.savez_compressed多数组打包、实验快照可命名、可压缩、整理方便比单个 npy 略慢一点np.savetxt / np.loadtxt给人看、跨工具交换可读性强通用丢元信息速度慢np.genfromtxt带表头、混合类型、缺失值容错能力最强解析偏慢参数多np.memmap / mmap_mode超大数组、分块读取按需加载内存友好只读或需手动 flush3. 实操过程从一个模拟项目说起3.1 先造一批像样的数据纸上谈兵不如完整跑一个小项目。设想一个很常见的场景你训练了一个线性模型特征矩阵 X 有 500 行 20 列标签 y 是 500 个 0/1 值训练完成后还得到一组模型权重 weights 和一个记录准确率的标量 acc。如果这些结果散落在内存里脚本一旦结束就什么都没了把这个流程存下来后续不仅可以直接复用中间结果还能用不同脚本对比几次实验之间的差异。import numpy as np rng np.random.default_rng(42) X rng.random((500, 20)).astype(np.float32) y rng.integers(0, 2, size500) weights rng.random(20).astype(np.float32) acc 0.873这里刻意把 X 和 weights 转成float32是为了模拟真实项目里常见的存储优化需求。如果从头到尾都用float64文件会大一倍而很多模型场景下精度完全够用。这是一个容易被忽视的存储优化点保存时明确 dtype比读出来之后再转换要省事得多。3.2 保存与读取的完整链路接着把这四个对象全部打进同一个压缩文件一方面演示.npz的打包能力另一方面也减少文件数量np.savez_compressed( simulation_out.npz, XX, yy, weightsweights, accacc )读取的时候我们关心三件事文件是否存在、键名是否齐全、数值是否与原来一致。import os if os.path.exists(simulation_out.npz): with np.load(simulation_out.npz) as data: X2 data[X] y2 data[y] w2 data[weights] acc2 data[acc] print(keys:, data.files) print(X2 shape:, X2.shape, dtype:, X2.dtype) print(acc:, acc2) else: print(文件不存在先运行保存部分)我这里用with打开了.npz文件。很多人不习惯这个写法但np.load返回的对象占用着文件句柄如果脚本里频繁轮换读取多个文件而不关闭在 Windows 上很容易遇到“文件被占用”的问题。习惯性写成with np.load(...) as data:系统会自动释放句柄逻辑也更清晰。之后的校验环节我会对关键字段做一次逐位比较而不是只看一眼 shape。因为文件读写大部分时候能正常完成但不代表没有发生类型更改或精度截断。比较时可以这样写print(np.array_equal(X, X2)) # 精确比对 print(np.max(np.abs(y - y2))) # 标签是整数差值必须为 0如果比对结果不一致几乎可以肯定是保存之前 dtype 就已经变化了常见原因是某一步astype被隐式改回float64。遇到这种情况不要急着换存储格式先检查数组从生成到保存之间每一步做了什么操作。3.3 大文件怎么读内存映射和 memmap真实项目里总会遇到一个内存装不下的超大矩阵。比如一张 20000×20000 的 float32 网格数据按字节算是 1.6GB直接np.load会一次性读进内存不仅慢还可能把机器拖到卡死。这时候要么分块处理要么用内存映射。最省事的办法是在np.load里传mmap_moder它不会立即把全部数据读进内存而是把文件内容映射到虚拟内存只有等你真正访问某个区域时才把对应块加载进来mm_arr np.load(large_array.npy, mmap_moder) print(mm_arr.shape) row mm_arr[0, :] # 只读第一行内存占用极小 print(row[:5])注意这种模式下数组是只读的适合分析、统计、抽样不适合直接修改原文件。如果你想生成一个新的可写大文件可以换用np.memmapmm np.memmap(huge.dat, dtypenp.float32, modew, shape(10000, 10000)) mm[:] 1.0 mm.flush()np.memmap的行为很像一个普通 NumPy 数组但对它的写入并不会立刻全部落盘而是先在内存缓冲之后由操作系统按页写回。为了不让意外断电丢失太多数据记得在关键节点调用flush()。我一般会在写完全部内容之后调一次然后删除临时引用必要时再重新用moder验证一遍文件头和数据长度。使用memmap还有一个额外好处不同进程可以共享映射同一个文件在大规模并行计算场景里可以减少数据拷贝。4. 常见问题与排查技巧实录4.1 路径、文件名、跨平台折腾最容易让新手抓狂的问题是路径里带着中文或者空格。某些环境下读写非 ASCII 路径会失败现象是文件明明写出来了读的时候却提示找不到或者直接抛编码错误。我的处理办法有三个层次第一项目内小范围使用就保持全英文路径第二如果确实要支持中文路径把路径统一用pathlib.Path对象传给np.save再用str()转换后传给np.load避免手工拼接字符串第三遇到跨平台共享的文件优先确认操作系统与 Python 版本是否一致不一致时先复制到本地再读取不要直接挂网络盘操作。文件重名覆盖也是个高频问题。np.save默认静默覆盖已有文件如果你的参数调了好几次很容易把上一次结果冲掉。比较稳妥的做法是在保存前检查文件是否存在from pathlib import Path import time p Path(result.npy) if p.exists(): p.rename(Path(fresult_backup_{time.time():.0f}.npy))不要小看这个小动作我见过不少人因为覆盖旧结果而后悔花几个小时重算只是为了找回一个本来可以备份到的文件。4.2 读回来 shape 和 dtype 不对怎么办有些人读回.npy之后第一眼看到 shape 不对就怀疑文件坏了。其实.npy文件的 shape 信息保存在文件头里读出来不对最大的可能是保存时数组本身就不是你以为的维度。这种情况常见于数组生成时经历了ravel或squeeze的隐式变形或者在某一步操作里改变了轴顺序。排查时要回溯代码重点看数组在保存前最后一个关键操作做了什么。dtype 不对则是另一种常见情况。例如你明明记得存的是float32读回来却是float64通常意味着保存前数组的内存布局已经被np.asarray或某些广播操作悄悄转换过。这类问题不会报错但对后续计算可能产生性能影响所以我一贯的做法是在保存语句紧邻的上方加一行print(arr.dtype)确认“即将写入的”到底是什么类型而不是靠记忆判断。字节序问题也容易被忽略。跨平台偶尔会遇到一个大端序文件被小端序机器读取的情况现象是读取正常但数字大得离谱或者全是 0。这时候用arr.dtype.byteorder查一下必要时用arr.astype(arr.dtype.newbyteorder())转换。如今绝大多数数据都是小端序遇到这类问题先检查文件是否被其他程序改写过头区域。4.3 和 pickle、h5py 的关系与取舍说到数据存储很多人会问为什么不用 picklePickle 的优势是几乎什么对象都能存但它的代价也很明显受 Python 版本和类定义影响文件依赖运行环境换机器或升级库之后经常出现反序列化失败。NumPy 的.npy格式是跨 Python 版本、跨平台友好的只要 NumPy 能读你基本不需要关心原来那个数组是哪个类生成的。所以我只在快速保存 Python 对象中间态时用 pickle正式交付的数据一律走.npy/.npz。如果数据量大到要按块追加、或者包含复杂的层级化结构我更倾向用h5py这类 HDF5 方案。HDF5 支持多维数组、分块读写、压缩和层级化管理特别适合科学计算流程。但它的学习曲线和依赖复杂度也比 NumPy 内置方案高一些。对大多数人来说先掌握.npy/.npz等文件超过几个 GB、或者需要按切片随机访问时再上h5py也不迟。两种方案并不冲突很多项目里会同时存在中间结果用.npz最终归档用 HDF5。4.4 一套可以照用的自检清单保存前我会检查三件事第一数组 dtype 是否已经按项目约定设置好第二文件名是否能一眼看出内容和版本第三打包时是否给数组起了明确关键词。读取后我再检查三件事第一用assert或np.array_equal做一次数值校验第二确认 shape 和 dtype 与预期一致第三如果用了np.load打开.npz确认句柄已经关闭。这套清单看起来琐碎但每次都能在我赶进度时拦住低级错误。检查节点检查内容常用写法保存前dtype、文件名、覆盖风险print(arr.dtype)、Path.exists()保存前.npz键名是否明确都用keyarr形式读取后数值是否一致np.array_equal(arr, arr2)读取后shape 是否合理assert arr.shape (500, 20)读取后文件句柄是否释放with np.load(...) as data:包裹5. 最后分享一点我的使用习惯我这几年做数据处理存储习惯其实一直在调整。最早不管什么都往 CSV 里塞后来被大文件读写的速度折磨够了才转向.npy再后来做实验对比又发现每个结果都单独存.npy太散于是统一改成.npz打包。现在我的固定组合是单数组用np.save多数组用np.savez_compressed需要给人看才生成 CSV超过内存容量就用 mmap 读取。这套组合已经稳定用了很长时间遇到复杂结构再单独上 HDF5。最后一个小技巧给重要的.npz文件写一个版本号字段。如果数组内容本身没有合适的字段我会在保存时约定一个名为meta的键存一个小字典比如记录创建时间、NumPy 版本、源码脚本的名字。这样半年后回来看文件不会对着一个莫名其妙的out.npz发愣。数据存储看似只是几个函数调用真正决定数据能不能复用的其实是这一层“让人能看懂”的额外功夫。
阅读完成 · 觉得有帮助?
咨询建站