NumPy 数据持久化:save
, savez
, load
用法详解
数据是科学计算和数据分析的核心。在使用 Python 进行数值计算时,NumPy 数组是不可或缺的数据结构。然而,程序运行结束后,内存中的数据就会丢失。为了实现数据的持久化,即将计算结果保存到文件以便后续使用或分享,NumPy 提供了专门的函数来高效地保存和加载其特有的数组对象。
本文将深入探讨 NumPy 提供的用于保存和加载数组的核心函数:numpy.save
、numpy.savez
、numpy.savez_compressed
以及它们对应的加载函数 numpy.load
。我们将详细介绍它们的用法、参数、文件格式,以及使用时的注意事项和最佳实践。
1. NumPy 的原生保存格式:.npy
和 .npz
在深入函数细节之前,了解 NumPy 使用的两种主要原生文件格式非常重要:
-
.npy
格式: 这是 NumPy 保存单个数组的标准二进制格式。它设计的目标是存储一个任意的 NumPy 数组,包括其数据类型(dtype)、形状(shape)以及数据本身。这种格式保证了数组的精确保存,并且在加载时能够完全恢复原始数组,而不会丢失信息(如精度)。.npy
文件内部包含一个头部信息,描述了数组的元数据,后面紧跟着原始的数组数据。它是跨平台兼容的。 -
.npz
格式: 这是 NumPy 保存多个数组的标准格式。它实际上是一个使用zip
压缩的文件归档,其中每个条目都是一个.npy
文件,保存了一个独立的数组。这使得你可以将多个相关的 NumPy 数组打包到一个文件中进行保存和加载。savez_compressed
函数会进一步对.npz
文件中的.npy
条目进行 zlib 压缩,以节省存储空间。
使用这些原生格式的好处是:
* 高效: 相较于文本格式(如 CSV),二进制读写速度更快。
* 精确: 能够完全保留数组的 dtype 和 shape 信息,避免数据类型转换或精度丢失。
* 方便: 加载时直接得到 NumPy 数组对象,无需额外的解析步骤。
2. 保存单个数组:numpy.save
numpy.save
函数用于将一个 NumPy 数组以 .npy
格式保存到文件。
基本用法:
“`python
import numpy as np
创建一个 NumPy 数组
array_to_save = np.arange(100).reshape(10, 10)
print(“原始数组:\n”, array_to_save)
指定文件名并保存
filename = ‘my_array.npy’
np.save(filename, array_to_save)
print(f”\n数组已保存到文件: {filename}”)
“`
函数签名:
python
numpy.save(file, arr, allow_pickle=True, fix_imports=True)
参数详解:
-
file
:- 必需参数。
- 指定保存文件的路径(字符串)或一个已打开的文件对象(必须是二进制写入模式,如
'wb'
)。 - 如果是一个字符串路径,NumPy 会自动打开、写入并关闭文件。如果文件扩展名不是
.npy
,NumPy 会自动添加.npy
扩展名。
-
arr
:- 必需参数。
- 需要保存的 NumPy 数组。
-
allow_pickle
:- 可选参数,默认为
True
。 - 这是一个非常重要的安全参数。NumPy 在内部可能会使用 Python 的
pickle
模块来序列化对象数组(即 dtype 为object
的数组),或者在某些复杂情况下(如保存包含 non-NumPy 对象的结构化数组)也可能用到。 - 当
allow_pickle=True
时,如果数组需要,NumPy 会使用pickle
。 - 安全警告: 从一个未知或不受信任的来源加载一个带有
allow_pickle=True
保存的文件是非常危险的!因为pickle
可以序列化任意 Python 对象,包括可以执行任意代码的类实例。加载这样的文件可能导致安全漏洞。 - 建议: 如果你确定数组中只包含基本数值类型(整数、浮点数、布尔值等),并且不需要保存任何 Python 对象,建议将
allow_pickle
设置为False
。这样可以提高安全性,并可能略微提升性能。只有当你确实需要保存包含 Python 对象的object
数组时,才将其设置为True
,并且在加载时务必小心来源。
- 可选参数,默认为
-
fix_imports
:- 可选参数,默认为
True
。 - 这是一个与
pickle
相关的参数。当使用pickle
时,如果 pickled 的对象使用了旧模块名,fix_imports=True
会尝试将旧模块名映射到新模块名。这主要用于 Python 2 到 Python 3 的兼容性。在现代 Python 开发中,通常保持默认True
即可。
- 可选参数,默认为
使用文件对象进行保存:
除了直接指定文件名,你也可以使用一个已经打开的文件对象。这在需要将数据保存到非文件系统位置(如内存缓冲区、网络流)时非常有用。
“`python
import io
使用 BytesIO 对象模拟内存文件
memory_file = io.BytesIO()
array_to_save_2 = np.random.rand(5, 5)
将数组保存到内存文件
np.save(memory_file, array_to_save_2)
print(“\n数组已保存到内存文件对象。”)
内存文件对象现在包含了 .npy 格式的二进制数据
你可以通过 memory_file.getvalue() 获取这些数据
binary_data = memory_file.getvalue()
print(f”内存文件中的数据长度: {len(binary_data)} 字节”)
注意:要从 memory_file 加载,需要先 seek(0) 回到文件开头
memory_file.seek(0)
loaded_array_from_memory = np.load(memory_file)
print(“从内存文件加载的数组:\n”, loaded_array_from_memory)
“`
3. 加载单个数组:numpy.load
(for .npy)
numpy.load
函数用于从 .npy
、.npz
文件或 pickle 文件中加载数据。当加载 .npy
文件时,它会返回一个 NumPy 数组。
基本用法:
加载使用 np.save
创建的 .npy
文件:
“`python
假设 ‘my_array.npy’ 文件已经存在
filename = ‘my_array.npy’
加载数组
loaded_array = np.load(filename)
print(f”\n从文件 {filename} 加载的数组:\n”, loaded_array)
print(“加载数组的形状:”, loaded_array.shape)
print(“加载数组的数据类型:”, loaded_array.dtype)
验证加载的数据与原始数据是否一致
print(“原始数组与加载数组是否相等:”, np.array_equal(array_to_save, loaded_array))
“`
可以看到,np.load
成功恢复了数组的形状和数据类型。
函数签名:
numpy.load
的函数签名比较复杂,因为它需要处理 .npy
, .npz
和 pickle 格式。这里我们先关注与 .npy
加载相关的参数。
python
numpy.load(file, mmap_mode=None, allow_pickle=False, fix_imports=True, encoding='ASCII')
参数详解(针对 .npy 加载):
-
file
:- 必需参数。
- 指定加载文件的路径(字符串)或一个已打开的文件对象(必须是二进制读取模式,如
'rb'
)。 - 如果是一个字符串路径,NumPy 会自动打开、读取并关闭文件。
-
mmap_mode
:- 可选参数,默认为
None
。 - 这个参数用于指定内存映射模式(memory-mapping)。当处理非常大的数组,以至于无法完全加载到内存时,内存映射是一个非常有用的技术。它允许你像操作内存中的数组一样操作文件中的数据,但数据本身仍保留在磁盘上,只将需要的部分加载到内存。
- 可用的模式包括:
None
: 不进行内存映射,将整个数组加载到内存中。这是默认行为。'r'
: 以只读模式打开文件并进行内存映射。修改映射的数组会导致错误。'r+'
: 以读写模式打开文件并进行内存映射。对数组的修改会写回到文件中(但不会改变文件大小)。'w+'
: 以读写模式打开文件并进行内存映射。如果文件存在,会清空文件内容并创建一个新的数组;如果文件不存在,则创建新文件。对数组的修改会写回到文件中。'c'
: 以写时复制模式打开文件并进行内存映射。对数组的修改会创建一个私有的、驻留内存的副本,原始文件不会被修改。
- 注意: 内存映射创建的数组对象与普通数组对象在行为上略有不同(例如,可能不支持所有操作)。使用完毕后,如果使用了写模式(’r+’ 或 ‘w+’),需要考虑同步或关闭操作,但对于简单的加载通常只需加载后进行处理。
- 可选参数,默认为
-
allow_pickle
:- 可选参数,默认为
False
。 - 这是与
save
中的allow_pickle
参数对应的加载参数。 - 极度重要: 从 NumPy 1.16.3 和 1.17.0 开始,加载文件的默认
allow_pickle
值从True
改为了False
,以增强安全性。 - 如果你尝试加载一个使用
allow_pickle=True
保存的文件,并且该文件确实包含了 pickled 的 Python 对象,那么在allow_pickle=False
的情况下加载会失败并抛出ValueError
。 - 只有当你完全信任文件的来源,并且明确知道文件需要 pickle 来正确加载时,才应该将
allow_pickle
设置为True
。
- 可选参数,默认为
-
fix_imports
:- 可选参数,默认为
True
。 - 与
save
中的同名参数作用相同,用于处理 pickle 的模块兼容性。
- 可选参数,默认为
-
encoding
:- 可选参数,默认为
'ASCII'
。 - 此参数主要用于加载 Python 2 中保存的包含字符串的 pickle 数据。通常在加载
.npy
文件时不需要修改。
- 可选参数,默认为
使用内存映射加载:
“`python
假设 ‘my_array.npy’ 是一个很大的文件
loaded_mmap_array = np.load(‘my_array.npy’, mmap_mode=’r’)
print(“\n使用内存映射加载数组 (只读模式):”, loaded_mmap_array.shape, loaded_mmap_array.dtype)
注意:内存映射对象在文件关闭或程序退出时自动清理。
如果是写模式 (‘r+’ 或 ‘w+’), 可能需要显式同步或关闭
loaded_mmap_array.flush() # 同步修改到文件
del loaded_mmap_array # 可能有助于资源释放
“`
(这里省略了实际的文件创建和 mmap 加载的代码,因为需要一个足够大的文件来演示 mmap 的优势,但概念上如上所述。)
4. 保存多个数组:numpy.savez
和 numpy.savez_compressed
当你需要保存多个相关的 NumPy 数组时,.npz
格式是更合适的选择。numpy.savez
和 numpy.savez_compressed
都用于创建 .npz
文件。
numpy.savez
:
将多个数组保存到一个未压缩的 .npz
文件中。
基本用法:
savez
可以接受任意数量的数组作为位置参数,或者通过关键字参数指定数组名称。使用关键字参数是更推荐的方式,因为它允许你为数组指定有意义的名称。
“`python
array1 = np.arange(10)
array2 = np.random.rand(3, 4)
array3 = np.array([‘apple’, ‘banana’, ‘cherry’])
使用位置参数保存 (数组会被命名为 arr_0, arr_1, …)
np.savez(‘multiple_arrays_pos.npz’, array1, array2, array3)
print(“\n多个数组已使用位置参数保存到 multiple_arrays_pos.npz”)
使用关键字参数保存 (推荐)
np.savez(‘multiple_arrays_kw.npz’, linear=array1, random_matrix=array2, fruits=array3)
print(“多个数组已使用关键字参数保存到 multiple_arrays_kw.npz”)
“`
函数签名:
python
numpy.savez(file, *args, **kwds, allow_pickle=True, fix_imports=True)
参数详解:
-
file
:- 必需参数。
- 保存文件的路径(字符串)或一个已打开的文件对象(二进制写入模式
'wb'
)。如果扩展名不是.npz
,NumPy 会自动添加。
-
*args
:- 可选参数。
- 一个或多个 NumPy 数组。这些数组在
.npz
文件中将按顺序命名为'arr_0'
,'arr_1'
, 以此类推。
-
**kwds
:- 可选参数。
- 一个或多个关键字参数,其中关键字是数组的名称,值是对应的 NumPy 数组。例如
name1=array1, name2=array2
。这种方式允许你为每个数组指定一个描述性的名字,方便后续加载时识别。
-
allow_pickle
,fix_imports
:- 同
numpy.save
中的对应参数。同样需要注意allow_pickle
的安全风险。
- 同
numpy.savez_compressed
:
与 savez
类似,但会对 .npz
文件中的每个 .npy
条目进行 zlib 压缩。这在需要减小文件大小时非常有用,特别是当数组数据有冗余(如包含大量零或重复值)时,压缩效果会更好。代价是保存和加载时需要额外的 CPU 时间进行压缩和解压缩。
基本用法:
用法与 savez
完全相同,只需将函数名替换为 savez_compressed
。
“`python
使用关键字参数保存并压缩
np.savez_compressed(‘multiple_arrays_compressed_kw.npz’, linear=array1, random_matrix=array2, fruits=array3)
print(“多个数组已使用关键字参数保存并压缩到 multiple_arrays_compressed_kw.npz”)
“`
文件大小比较(示例):
你可以观察使用 savez
和 savez_compressed
生成的文件大小差异。
“`python
import os
print(“\n文件大小比较:”)
print(f”multiple_arrays_pos.npz: {os.path.getsize(‘multiple_arrays_pos.npz’)} 字节”)
print(f”multiple_arrays_kw.npz: {os.path.getsize(‘multiple_arrays_kw.npz’)} 字节”)
print(f”multiple_arrays_compressed_kw.npz: {os.path.getsize(‘multiple_arrays_compressed_kw.npz’)} 字节”)
``
_compressed` 版本的文件会更小,尤其对于可压缩性高的数据。
通常情况下,
5. 加载多个数组:numpy.load
(for .npz)
当 numpy.load
函数检测到加载的是一个 .npz
文件时,它不会直接返回一个数组,而是返回一个 NpzFile
对象。这是一个类似字典的对象,你可以通过数组的名称(在保存时指定的关键字参数名,或默认的 'arr_0'
等)来访问其中的每个数组。
基本用法:
“`python
加载使用关键字参数保存的 .npz 文件
loaded_npz = np.load(‘multiple_arrays_kw.npz’)
print(f”\n从 multiple_arrays_kw.npz 加载的对象类型: {type(loaded_npz)}”)
NpzFile 对象提供一个 .files 属性,列出包含的数组名称
print(“加载的 .npz 文件中包含的数组名称:”, loaded_npz.files)
通过数组名称访问单个数组
loaded_linear = loaded_npz[‘linear’]
loaded_random_matrix = loaded_npz[‘random_matrix’]
loaded_fruits = loaded_npz[‘fruits’]
print(“\n从 .npz 文件中按名称访问数组:”)
print(“linear:\n”, loaded_linear)
print(“random_matrix:\n”, loaded_random_matrix)
print(“fruits:\n”, loaded_fruits)
使用位置参数保存的文件,名称是 arr_0, arr_1, …
loaded_npz_pos = np.load(‘multiple_arrays_pos.npz’)
print(“\n从 multiple_arrays_pos.npz 加载的对象中包含的数组名称:”, loaded_npz_pos.files)
loaded_array_0 = loaded_npz_pos[‘arr_0’]
loaded_array_1 = loaded_npz_pos[‘arr_1’]
… 访问其他数组
访问完后,应该关闭 NpzFile 对象,尤其是在不使用 ‘with’ 语句时
loaded_npz.close()
loaded_npz_pos.close()
print(“\nNpzFile 对象已关闭。”)
“`
使用 with
语句加载 (推荐):
NpzFile
对象实现了 Python 的上下文管理协议,因此强烈推荐使用 with
语句来加载 .npz
文件。这样可以确保文件在代码块执行完毕后被正确关闭,释放资源。
“`python
使用 with 语句加载 .npz 文件
with np.load(‘multiple_arrays_kw.npz’) as data:
print(“\n使用 with 语句加载 .npz 文件:”)
print(“包含的数组名称:”, data.files)
# 在 with 块内访问数组
loaded_linear_with = data['linear']
loaded_random_matrix_with = data['random_matrix']
print("在 with 块内访问 linear 数组:\n", loaded_linear_with)
一旦退出 with 块,NpzFile 对象会自动关闭,不能再访问其内容
try:
print(data[‘fruits’]) # 这行代码会引发 ValueError,因为文件已关闭
except ValueError as e:
print(f”\n尝试在 with 块外访问 data 对象时发生错误: {e}”)
“`
参数详解(针对 .npz 加载):
numpy.load
加载 .npz
文件时,同样接受 mmap_mode
, allow_pickle
, fix_imports
, encoding
参数。
mmap_mode
: 理论上可以使用,但对于.npz
文件中的每个独立.npy
条目进行内存映射不如直接对大型.npy
文件进行内存映射常见。allow_pickle
: 同.npy
加载,默认为False
,加载包含 pickled 对象的.npz
文件时需要设置为True
。fix_imports
,encoding
: 主要与 pickle 相关,通常保持默认。
6. 高级注意事项和最佳实践
allow_pickle
安全性: 再次强调,从不受信任的来源加载文件时,务必将allow_pickle
设置为False
。如果必须处理可能包含 pickled 数据的旧文件或特定类型的文件,请确保你了解风险并信任数据来源。对于大多数涉及纯数值数据的 NumPy 数组,将allow_pickle
设置为False
是更安全的做法,并且在 NumPy 新版本中,这已经是默认值。- 使用
with
语句: 加载.npz
文件时,总是优先使用with np.load(...) as data:
语法。这是一种良好的资源管理实践,确保文件被正确关闭,避免潜在的资源泄露问题。 - 选择
savez
vssavez_compressed
: 如果磁盘空间是一个重要考虑因素,并且数据具有一定的冗余度,使用savez_compressed
可以显著减小文件大小。如果 CPU 性能更关键,或者数据压缩效果不佳,那么使用savez
(未压缩)可以获得更快的保存和加载速度。 - 内存映射 (
mmap_mode
): 仅在处理非常大的.npy
文件,且不需要一次性将整个数组加载到内存时考虑使用内存映射。它允许你在文件上进行部分操作,而无需消耗大量内存。理解不同映射模式 ('r'
,'r+'
,'w+'
,'c'
) 的区别对于正确使用非常重要。 - 文件对象的使用: 直接使用文件对象作为
file
参数,可以让你更灵活地控制数据的来源和目的地,例如从网络连接读取数据,或者将数据直接写入内存缓冲区,而无需创建临时文件。 - 与其他格式的比较:
.npy
和.npz
格式是 NumPy 原生的、最高效的保存和加载 NumPy 数组的方式。如果需要与其他非 NumPy 系统或语言交换数据,可能需要考虑其他更通用的格式,如 CSV(文本,简单但效率低,精度损失)、JSON(文本,灵活但效率低,不适合大型数值数组)、HDF5(二进制,高效,支持复杂结构和元数据,跨语言支持)。选择哪种格式取决于你的具体需求:如果只是在 Python/NumPy 环境内部进行数据持久化,.npy
/.npz
是最佳选择。
7. 总结
本文详细介绍了 NumPy 提供的用于数组持久化的核心函数:numpy.save
、numpy.savez
、numpy.savez_compressed
和 numpy.load
。
numpy.save
将单个 NumPy 数组保存为.npy
文件。numpy.savez
将多个 NumPy 数组保存为未压缩的.npz
文件。numpy.savez_compressed
将多个 NumPy 数组保存为压缩的.npz
文件。numpy.load
用于加载.npy
和.npz
文件,对于.npz
文件,它返回一个NpzFile
对象,可以通过字典方式访问其中的数组。
我们深入探讨了各个函数的参数,特别是 allow_pickle
的安全含义和 mmap_mode
在处理大型文件时的作用。掌握这些函数及其参数,能够帮助你高效、安全地保存和加载 NumPy 数组,是进行大规模数值计算和数据处理的基础技能。记住在处理来自外部或不受信任来源的数据时,始终警惕 allow_pickle=True
可能带来的安全风险。在加载 .npz
文件时,优先使用 with
语句管理资源。
通过合理地使用这些函数,你可以轻松地在程序运行之间保存计算状态,共享数据集,或者处理超出内存容量的大型数组。