NumPy: 文件IO
难度:⭐⭐ | 关键词:
np.save,np.load,np.savez,np.savetxt,np.loadtxt,np.memmap
Bob 每次运行模型花 10 分钟重新生成数据。Alice:"np.save 第一次存成 .npy,以后 np.load 0.1 秒加载。二进制比文本快 10 倍、小 3 倍。"
1. save / load — 二进制单数组
np.save(file, arr) 将数组存为 .npy 二进制文件;np.load(file) 读回。
| 参数 | 说明 |
|---|---|
file |
文件路径,自动补 .npy 后缀 |
arr |
要保存的 ndarray |
allow_pickle |
np.load 参数,是否允许反序列化 pickle,默认 False |
(1) .npy 格式特点
- 二进制存储,保留 dtype、shape、字节序等元信息
- 加载时无需指定格式,
np.load自动还原 - 文件小、读写快,跨平台兼容
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:save / load(难度⭐)
import numpy as np
data = np.arange(12).reshape(3, 4)
np.save("my_array.npy", data)
loaded = np.load("my_array.npy")
print(loaded)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(np.array_equal(data, loaded)) # True
# save 会自动补 .npy 后缀,下面两行等价
np.save("my_array", data) # 实际保存为 my_array.npy
np.save("my_array.npy", data) # 同上
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
2. savez / savez_compressed — 多数组打包
np.savez(file, *args, **kwargs) 将多个数组存入一个 .npz 文件(ZIP 格式)。
| 函数 | 说明 | 压缩 |
|---|---|---|
np.savez |
多数组打包为 .npz |
❌ |
np.savez_compressed |
同上,但启用压缩 | ✅ |
(1) savez 用法
import numpy as np
# 位置参数 → 自动命名 arr_0, arr_1, ...
a = np.arange(6).reshape(2, 3)
b = np.array([1, 2, 3])
np.savez("data.npz", a, b)
# 查看键名:np.load("data.npz").files → ['arr_0', 'arr_1']
# 关键字参数 → 用键名访问
weights = np.array([0.5, 0.3])
biases = np.array([0.1, 0.2])
np.savez("params.npz", weights=weights, biases=biases)
loaded = np.load("params.npz")
print(loaded["weights"]) # [0.5 0.3]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:savez 多数组(难度⭐)
import numpy as np
weights = np.random.randn(3, 4)
biases = np.random.randn(4)
labels = np.array([0, 1, 2, 1])
np.savez("model.npz", weights=weights, biases=biases, labels=labels)
data = np.load("model.npz")
print(data.files) # ['weights', 'biases', 'labels']
print(data["weights"].shape) # (3, 4)
print(data["biases"].shape) # (4,)
data.close()
# 或用 with 语句
with np.load("model.npz") as data:
w = data["weights"]
b = data["biases"]
print(np.array_equal(w, weights)) # True
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) savez vs savez_compressed 对比
import numpy as np
import os
arr = np.random.randn(10000, 100)
np.savez("uncompressed.npz", arr=arr)
np.savez_compressed("compressed.npz", arr=arr)
size_raw = os.path.getsize("uncompressed.npz")
size_zip = os.path.getsize("compressed.npz")
print(f"Uncompressed: {size_raw / 1e6:.2f} MB")
print(f"Compressed: {size_zip / 1e6:.2f} MB")
print(f"Ratio: {size_zip / size_raw:.2%}")
# Uncompressed: ~8.00 MB
# Compressed: ~7.60 MB (随机数据压缩率低)
# Ratio: ~95%
# 规律数据压缩效果显著
pattern = np.arange(1_000_000)
np.savez("pat_raw.npz", arr=pattern)
np.savez_compressed("pat_zip.npz", arr=pattern)
print(f"Pattern compressed ratio: "
f"{os.path.getsize('pat_zip.npz') / os.path.getsize('pat_raw.npz'):.2%}")
# Pattern compressed ratio: ~10%
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
3. savetxt / loadtxt — 文本格式
np.savetxt(fname, X, fmt='%.18e', delimiter=' ') 保存为文本;np.loadtxt(fname, delimiter=None) 读回。
(1) 格式对比
| 格式 | 扩展名 | 保留元信息 | 读写速度 | 文件大小 | 可读性 |
|---|---|---|---|---|---|
.npy |
.npy |
✅ dtype/shape | ⚡ 极快 | 🟢 小 | ❌ |
.npz |
.npz |
✅ 多数组 | ⚡ 快 | 🟡 中 | ❌ |
.txt |
.txt |
❌ | 🐢 慢 | 🔴 大 | ✅ |
.csv |
.csv |
❌ | 🐢 慢 | 🔴 大 | ✅ |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:savetxt / loadtxt(难度⭐⭐)
import numpy as np
data = np.array([[1.0, 2.5, 3.7],
[4.2, 5.8, 6.1],
[7.3, 8.9, 9.0]])
np.savetxt("data.txt", data)
np.savetxt("data.csv", data, fmt="%.2f", delimiter=",")
loaded_txt = np.loadtxt("data.txt")
loaded_csv = np.loadtxt("data.csv", delimiter=",")
print(loaded_csv)
# [[1. 2.5 3.7 ]
# [4.2 5.8 6.1 ]
# [7.3 8.9 9. ]]
np.savetxt("header.csv", data, fmt="%.2f", delimiter=",",
header="col1,col2,col3", comments="")
loaded_skip = np.loadtxt("header.csv", delimiter=",", skiprows=1)
print(np.array_equal(loaded_skip, data)) # True
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) save vs savetxt 性能对比
import numpy as np
import time
import os
big = np.random.randn(100000, 50)
t0 = time.perf_counter()
np.save("perf.npy", big)
t_save = time.perf_counter() - t0
t0 = time.perf_counter()
np.savetxt("perf.txt", big, fmt="%.6f")
t_txt = time.perf_counter() - t0
print(f"save: {t_save:.3f}s size={os.path.getsize('perf.npy')/1e6:.1f}MB")
print(f"savetxt: {t_txt:.3f}s size={os.path.getsize('perf.txt')/1e6:.1f}MB")
print(f"Text is {t_txt/t_save:.0f}x slower, "
f"{os.path.getsize('perf.txt')/os.path.getsize('perf.npy'):.1f}x larger")
# save: ~0.05s size=38.1MB
# savetxt: ~2.50s size=135.0MB
# Text is ~50x slower, ~3.5x larger
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
4. genfromtxt — 缺失值处理
np.genfromtxt(fname, delimiter=None, missing_values=None, filling_values=None) 是 loadtxt 的增强版,能处理缺失值和异构列。
(1) genfromtxt vs loadtxt
| 特性 | np.loadtxt |
np.genfromtxt |
|---|---|---|
| 缺失值处理 | ❌ 遇到缺失直接报错 | ✅ 可指定填充值 |
| 列名读取 | ❌ | ✅ names=True |
| 指定列类型 | ❌ | ✅ dtype=None 自动推断 |
| 速度 | 较快 | 较慢(功能更多) |
| 适用场景 | 干净整齐的文本 | 有缺失/表头的 CSV |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:genfromtxt 缺失值(难度⭐⭐)
import numpy as np
from io import StringIO
csv = """name,score,age
Alice,85,20
Bob,,22
Charlie,90,
Carol,78,21
"""
data = np.genfromtxt(StringIO(csv), delimiter=",", names=True,
dtype=None, encoding=None, missing_values="",
filling_values=-1)
print(data)
# [('Alice', 85., 20) ('Bob', -1., 22) ('Charlie', 90., -1) ('Carol', 78., 21)]
print(data["name"]) # ['Alice' 'Bob' 'Charlie' 'Carol']
print(data["score"]) # [85. -1. 90. 78.]
print(data["age"]) # [20 22 -1 21]
data2 = np.genfromtxt(StringIO(csv), delimiter=",", names=True,
dtype=None, encoding=None, missing_values="",
filling_values=("unknown", 0, -1))
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) CSV 读写实战
import numpy as np
rng = np.random.default_rng(42)
records = np.column_stack([
np.arange(1, 6),
rng.integers(60, 100, size=5),
rng.integers(18, 25, size=5)
])
np.savetxt("students.csv", records, fmt="%d", delimiter=",",
header="id,score,age", comments="")
data = np.loadtxt("students.csv", delimiter=",", skiprows=1,
dtype=[("id", int), ("score", int), ("age", int)])
print(data["id"]) # [1 2 3 4 5]
print(data["score"]) # [88 78 91 65 86]
print(data["age"]) # [21 23 20 24 19]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
5. fromfile / tofile — 底层二进制
ndarray.tofile(fid) 和 np.fromfile(file, dtype) 是最底层的二进制读写,不保存元信息。
| 特性 | save/load |
tofile/fromfile |
|---|---|---|
| 保存元信息 | ✅ dtype/shape | ❌ 裸数据 |
| 跨平台安全 | ✅ | ⚠️ 字节序可能不同 |
| 适用场景 | 通用 | 已知 dtype/shape 的固定格式 |
import numpy as np
arr = np.arange(6, dtype=np.float64)
arr.tofile("raw.bin")
loaded = np.fromfile("raw.bin", dtype=np.float64)
print(loaded) # [0. 1. 2. 3. 4. 5.]
# shape 信息丢失,需要手动 reshape
arr2d = np.arange(12).reshape(3, 4)
arr2d.tofile("raw2d.bin")
loaded2d = np.fromfile("raw2d.bin", dtype=np.int64).reshape(3, 4)
print(np.array_equal(arr2d, loaded2d)) # True
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
6. memmap — 大文件映射
np.memmap(filename, dtype, mode, shape) 将磁盘文件映射为数组,不一次性读入内存。
(1) 大文件策略对比
| 策略 | 内存占用 | 速度 | 适用场景 |
|---|---|---|---|
np.load |
全部加载到内存 | ⚡ | 数据量 < 可用内存 |
np.memmap |
按需加载,虚拟内存 | 🟡 | 数据量 > 内存 |
| 分块读写 | 手动控制 | 🟡 | 超大数据流水线 |
| HDF5 (h5py) | 按需加载 + 切片 | ⚡ | 多维超大数据 |
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:memmap 大文件(难度⭐⭐⭐)
import numpy as np
import os
filename = "bigdata.dat"
shape = (100000, 1000) # 1 亿 float64 ≈ 800MB
fp = np.memmap(filename, dtype=np.float64, mode="w+", shape=shape)
fp[:10] = np.arange(10).reshape(10, 1) * np.ones(1000)
del fp # 关闭映射,数据写入磁盘
print(f"File size: {os.path.getsize(filename) / 1e6:.0f} MB")
fp_read = np.memmap(filename, dtype=np.float64, mode="r", shape=shape)
print(fp_read[0, :5]) # [0. 0. 0. 0. 0.]
print(fp_read[5, :5]) # [5. 5. 5. 5. 5.]
fp_rw = np.memmap(filename, dtype=np.float64, mode="r+", shape=shape)
fp_rw[0, 0] = 999.0
fp_rw.flush()
fp_check = np.memmap(filename, dtype=np.float64, mode="r", shape=shape)
print(fp_check[0, 0]) # 999.0
del fp_read, fp_rw, fp_check
os.remove(filename)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) IO 决策树
graph TB
A["需要保存/加载数组"] --> B{"需要可读性?"}
B -->|是| C{"有缺失值?"}
B -->|否| D{"单个还是多个数组?"}
C -->|否| E["savetxt / loadtxt"]
C -->|是| F["genfromtxt"]
D -->|单个| G{"数据 > 内存?"}
D -->|多个| H{"需要压缩?"}
G -->|否| I["save / load (.npy)"]
G -->|是| J["memmap"]
H -->|否| K["savez (.npz)"]
H -->|是| L["savez_compressed (.npz)"]
style A fill:#4CAF50,color:#fff
style I fill:#2196F3,color:#fff
style J fill:#FF9800,color:#fff
style K fill:#9C27B0,color:#fff
style L fill:#E91E63,color:#fff
style E fill:#00BCD4,color:#fff
style F fill:#795548,color:#fff
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
7. 综合示例:数据持久化流水线
Alice 演示完整的数据 IO 流程:生成 → 保存二进制 → 读取 → 读写 CSV → memmap 处理超大数据。
import numpy as np
import os
from io import StringIO
rng = np.random.default_rng(seed=42)
# ========================================
# Part 1: 生成数据并二进制保存
# ========================================
print("=== Part 1: Binary Save/Load ===\n")
features = rng.normal(0, 1, size=(10000, 50))
labels = rng.integers(0, 10, size=10000)
np.save("features.npy", features)
np.save("labels.npy", labels)
feat_loaded = np.load("features.npy")
print(f"Features shape: {feat_loaded.shape}")
print(f"Data matches: {np.array_equal(feat_loaded, features)}")
# ========================================
# Part 2: savez 打包多数组
# ========================================
print("\n=== Part 2: savez Multiple Arrays ===\n")
np.savez("dataset.npz", features=features, labels=labels)
np.savez_compressed("dataset_z.npz", features=features, labels=labels)
size_raw = os.path.getsize("dataset.npz")
size_zip = os.path.getsize("dataset_z.npz")
print(f"Raw: {size_raw / 1e6:.2f} MB")
print(f"Compressed: {size_zip / 1e6:.2f} MB ({size_zip/size_raw:.0%} of raw)")
with np.load("dataset.npz") as d:
print(f"Arrays in npz: {d.files}")
print(f"Features match: {np.array_equal(d['features'], features)}")
# ========================================
# Part 3: CSV 读写
# ========================================
print("\n=== Part 3: CSV Read/Write ===\n")
sample = features[:5, :4]
header = "f0,f1,f2,f3"
np.savetxt("sample.csv", sample, fmt="%.4f", delimiter=",",
header=header, comments="")
csv_data = np.loadtxt("sample.csv", delimiter=",", skiprows=1)
print(f"CSV shape: {csv_data.shape}")
print(f"CSV matches: {np.allclose(csv_data, sample)}")
# ========================================
# Part 4: genfromtxt 读取含缺失 CSV
# ========================================
print("\n=== Part 4: genfromtxt with Missing ===\n")
csv_missing = """id,score,grade
1,85,A
2,,B
3,92,
4,78,C
"""
data = np.genfromtxt(StringIO(csv_missing), delimiter=",",
names=True, dtype=None, encoding=None,
filling_values=(0, -1, "X"))
print(f"IDs: {data['id']}")
print(f"Scores: {data['score']}")
print(f"Grades: {data['grade']}")
# ========================================
# Part 5: memmap 处理超大数据
# ========================================
print("\n=== Part 5: memmap Large Data ===\n")
big_shape = (50000, 500)
big_file = "bigdata.dat"
fp = np.memmap(big_file, dtype=np.float64, mode="w+", shape=big_shape)
fp[:] = rng.standard_normal(big_shape)
fp.flush()
del fp
fp_read = np.memmap(big_file, dtype=np.float64, mode="r", shape=big_shape)
col_mean = fp_read[:, 0].mean()
print(f"File size: {os.path.getsize(big_file)/1e6:.0f} MB")
print(f"Column 0 mean: {col_mean:.4f} (expect ~0)")
del fp_read
for f in ["features.npy", "labels.npy", "dataset.npz",
"dataset_z.npz", "sample.csv", "bigdata.dat"]:
if os.path.exists(f):
os.remove(f)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
.npy 存单个数组,.npz 是 ZIP 格式打包多个数组。np.save → .npy,np.savez → .npz。.npz 加载后返回类似字典的 NpzFile 对象,通过键名访问各数组。loadtxt(更快);有缺失值、需要列名、或列类型不一致时用 genfromtxt。genfromtxt 功能更多但速度更慢。memmap 利用操作系统的虚拟内存机制按需加载,只在访问时读取对应磁盘区域。适合处理超大数组的局部操作,但随机访问较多时仍然会频繁换页,性能下降。savez 更快。tofile/fromfile 是裸二进制,不保存 dtype 和 shape,读回时必须手动指定且需知道原始形状,还有字节序风险。save/load 内含完整元信息,安全且推荐日常使用。📖 小节
np.save/np.load— 单数组二进制,保留元信息,日常首选np.savez— 多数组打包为 .npz,不压缩np.savez_compressed— 同 savez 但压缩,省空间np.savetxt/np.loadtxt— 文本格式,可读但慢且丢元信息np.genfromtxt— 增强版 loadtxt,支持缺失值和列名np.memmap— 大文件内存映射,按需加载不占内存ndarray.tofile/np.fromfile— 裸二进制,无元信息,底层专用
📝 作业
(1) 三个数组存入 .npz
- 创建 3 个数组:
weights = rng.randn(10, 5)、biases = rng.randn(5)、labels = np.array([0,1,2,3,4]) - 用
np.savez保存为model.npz - 用
np.load读回,验证 3 个数组与原始一致
(2) loadtxt 读取 CSV
- 用
np.savetxt将以下数据保存为scores.csv(含表头math,english,science):
90,85,88
78,92,80
95,88,93
- 用
np.loadtxt读取(跳过表头),计算每科平均分
(3) .npy vs .txt 大小对比
- 生成
rng.random(size=(10000, 100))的大数组 - 分别用
np.save和np.savetxt保存 - 比较两种文件的大小,计算文本文件是二进制的几倍
- 比较读写耗时(用
time.perf_counter)