1.8 数组输入输出(I/O)
处理数据的第一步通常是读取外部数据,处理完再保存结果。NumPy 提供多种 I/O 方式:文本文件适合与其他工具交换数据、人类可读;二进制文件读取快、占用小、能完整保留 dtype 和形状。本节将讲解文本文件读写、二进制文件保存加载、内存数据与字节流的转换,以及 pickle 序列化的支持。
1.8.1 文本文件读写
np.savetxt() 保存文本
np.savetxt 把数组写入文本文件,可以指定分隔符、格式、表头:
import numpy as np
a = np.array([[1.5, 2.5], [3.5, 4.5]])
np.savetxt("data.csv", a, delimiter=",", fmt="%.2f")
# 生成 data.csv:
# 1.50,2.50
# 3.50,4.50np.loadtxt() 读取文本
np.loadtxt 读取纯数值文本文件,是最简单的读取方式:
import numpy as np
# 读取上述 CSV
data = np.loadtxt("data.csv", delimiter=",")
print(data)
# [[1.5 2.5]
# [3.5 4.5]]loadtxt 支持 skiprows 跳过表头行、usecols 选择列:
# 跳过第一行表头,只读第 0、2 列
data = np.loadtxt("data.csv", delimiter=",", skiprows=1, usecols=(0, 2))np.genfromtxt() 通用读取
np.genfromtxt 比 loadtxt 更强大:支持混合类型、缺失值处理、指定 dtype。它适合读取含文本列和缺失值的真实表格:
import numpy as np
# 读取含名称列和缺失值的 CSV
data = np.genfromtxt(
"people.csv",
delimiter=",",
names=True, # 第一行作为字段名
dtype=None, # 自动推断每列类型
encoding="utf-8",
)
print(data["age"]) # 按字段名访问genfromtxt 的关键参数:names=True 把首行当表头、dtype=None 逐列推断类型、missing_values/filling_values 处理缺失数据。它对真实、不规整的数据更稳健,是首选读取函数。
1.8.2 二进制文件
np.save() 与 np.load()
np.save 以 NumPy 的 .npy 二进制格式保存单个数组,np.load 读取。二进制格式完整保留 dtype、形状,读取速度快、占用小:
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
np.save("arr.npy", a)
loaded = np.load("arr.npy")
print(loaded)
# [[1 2 3]
# [4 5 6]]
print(loaded.dtype) # int64,dtype 完整保留.npy 文件是 NumPy 专用格式,其他程序不能直接读,但同一项目内使用效率最高。
np.savez() 保存多个数组
np.savez 把多个数组压缩保存到一个 .npz 文件,支持用名字区分:
import numpy as np
x = np.array([1, 2, 3])
y = np.array([[4, 5], [6, 7]])
np.savez("data.npz", x=x, y=y) # 传关键字参数命名
data = np.load("data.npz")
print(data["x"]) # [1 2 3]
print(data["y"])
# [[4 5]
# [6 7]]np.load 返回一个类似字典的对象,通过关键字名访问各数组。
np.savez_compressed()
np.savez_compressed 与 savez 相同,但额外做压缩,文件更小、读写稍慢:
import numpy as np
x = np.arange(100000)
np.savez_compressed("big.npz", x=x) # 比 savez 文件更小
data = np.load("big.npz")
print(data["x"].shape) # (100000,)读取 .npz 时注意
.npz 是压缩归档,np.load 默认惰性加载,数组在首次访问时才读入内存,适合含多个大数组的场景。
1.8.3 内存数据转字节
.tobytes()
数组的 .tobytes() 方法把内存数据直接导出为原始字节流,不包含形状和 dtype 信息:
import numpy as np
a = np.array([1, 2, 3], dtype=np.int32)
raw = a.tobytes()
print(len(raw)) # 12,3 个 int32 各 4 字节np.frombuffer()
np.frombuffer 从字节流创建数组(只读视图):
import numpy as np
raw = bytes([1, 0, 0, 0, 2, 0, 0, 0]) # 两个 int32
arr = np.frombuffer(raw, dtype=np.int32)
print(arr) # [1 2]frombuffer 生成的是只读数组,需要可写时 .copy()。
ndarray.tofile()
.tofile() 把数组以原始二进制写入文件,不保存形状和 dtype,加载时需要手动指定:
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
a.tofile("raw.bin")
# 读取时指定 dtype 和形状
data = np.fromfile("raw.bin", dtype=np.int32)
print(data) # [1 2 3 4 5 6],一维
data2 = data.reshape(2, 3)
print(data2)
# [[1 2 3]
# [4 5 6]]tofile/fromfile 是最底层的二进制 I/O,不保存元数据,适合与其他语言或系统交换原始数据。
用 frombuffer 创建可写数组
frombuffer 返回只读,可先用 copy():
import numpy as np
raw = bytearray(12) # 12 字节
arr = np.frombuffer(raw, dtype=np.int32) # 只读视图
print(arr) # [0 0 0]1.8.4 对象序列化(pickle)支持
用 pickle 保存任意对象
Python 的 pickle 模块能序列化任意 Python 对象,包括 NumPy 数组:
import numpy as np
import pickle
a = np.array([1, 2, 3])
with open("arr.pkl", "wb") as f:
pickle.dump(a, f)
with open("arr.pkl", "rb") as f:
loaded = pickle.load(f)
print(loaded) # [1 2 3]pickle 与 np.save 的选择
pickle 可以保存任何对象(含自定义类、混合结构),但存在安全问题:加载不可信的 pickle 文件会执行任意代码。NumPy 数组的常规保存应优先用 np.save/np.load,格式紧凑且安全;需要保存复杂 Python 对象组合时才用 pickle。
import numpy as np
import pickle
# 保存混合结构(数组 + 字典)
data = {"x": np.array([1, 2]), "name": "task", "flag": True}
with open("obj.pkl", "wb") as f:
pickle.dump(data, f)
with open("obj.pkl", "rb") as f:
obj = pickle.load(f)
print(obj["x"]) # [1 2]
print(obj["name"]) # task练习题
第1题 概念理解
np.save/np.load 与 np.savetxt/np.loadtxt 有什么区别?各自适合什么场景?.npy 与 .npz 有何不同?
参考答案
save/load 使用二进制 .npy 格式,完整保留 dtype、形状,读取快、占用小,是 NumPy 内部数据交换的首选;savetxt/loadtxt 是文本格式,人类可读、可被其他工具(Excel、R)读取,但丢失 dtype 且读写慢。.npy 只存单个数组,.npz 压缩存储多个数组。
第2题 代码编写
创建数组 a = np.arange(20).reshape(4, 5),分别用 savetxt 保存为逗号分隔的 CSV 文件并用 loadtxt 读回;再用 savez_compressed 保存两个数组并用 load 读取。
参考答案
import numpy as np
a = np.arange(20).reshape(4, 5)
np.savetxt("a.csv", a, delimiter=",")
b = np.loadtxt("a.csv", delimiter=",")
print(b.shape) # (4, 5)
x = np.arange(5)
y = np.ones(5)
np.savez_compressed("xy.npz", x=x, y=y)
data = np.load("xy.npz")
print(data["x"]) # [0 1 2 3 4]
print(data["y"]) # [1. 1. 1. 1. 1.]第3题 进阶练习
数组 a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32) 用 tofile 写出原始字节,然后用 fromfile 读回并还原形状。说明 tofile 丢失了哪些信息,以及如何避免信息丢失。
参考答案
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
a.tofile("raw.bin")
data = np.fromfile("raw.bin", dtype=np.int32)
print(data) # [1 2 3 4 5 6],一维
data2 = data.reshape(2, 3)
print(data2.shape) # (2, 3)
# tofile 丢失形状和 dtype 信息,读回需手动指定 dtype 并 reshape
# 如需保留元数据,用 np.save/np.load 保存 .npy 格式tofile 只写原始数据,不写形状和 dtype;save 会把这些元数据一起写入 .npy,读取时无需手动恢复。
第4题 项目实践
命令行任务管理器要把任务列表保存到磁盘。请设计:用 np.savez_compressed 保存任务的完成度数组和优先级数组两个数组,并用 np.load 读取后验证两个数组形状一致。
参考答案
import numpy as np
completion = np.array([0.2, 0.5, 0.9, 0.1])
priority = np.array([3, 1, 5, 2])
np.savez_compressed("tasks.npz", completion=completion, priority=priority)
data = np.load("tasks.npz")
print(data["completion"]) # [0.2 0.5 0.9 0.1]
print(data["priority"]) # [3 1 5 2]
print(data["completion"].shape == data["priority"].shape) # True常见错误
错误 1 · ValueError: could not convert string to float: 'abc'
原因:用 loadtxt 读取含文本列的文件,它默认只处理数值,遇到字符串报错。
解决:含文本或缺失数据时改用 np.genfromtxt 并设置 dtype=None 逐列推断类型。
错误 2 · 用 fromfile 读回时形状或 dtype 错误
原因:tofile/fromfile 不保存形状和 dtype,读回的数据是一维的,类型也不确定。
解决:读取时显式传 dtype,并手动 reshape 恢复形状;需要保留元数据时改用 np.save。
错误 3 · np.load 报 ValueError: Cannot load file containing pickled data
原因:文件可能是旧格式或经过 pickle 处理,默认不允许反序列化。
解决:确保文件是 .npy/.npz 标准格式;确需加载 pickle 数据时用 np.load(..., allow_pickle=True)(注意安全风险)。
错误 4 · 加载不可信的 pickle 文件执行任意代码
原因:pickle 反序列化可以执行恶意代码,加载来源不明的 .pkl 文件存在安全风险。
解决:只用可信来源的 pickle 文件;与外部交换数据优先用 np.save/np.load 或文本格式。