NumPy Save 用法详解 – wiki基地


NumPy 数据持久化:save, savez, load 用法详解

数据是科学计算和数据分析的核心。在使用 Python 进行数值计算时,NumPy 数组是不可或缺的数据结构。然而,程序运行结束后,内存中的数据就会丢失。为了实现数据的持久化,即将计算结果保存到文件以便后续使用或分享,NumPy 提供了专门的函数来高效地保存和加载其特有的数组对象。

本文将深入探讨 NumPy 提供的用于保存和加载数组的核心函数:numpy.savenumpy.saveznumpy.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.saveznumpy.savez_compressed

当你需要保存多个相关的 NumPy 数组时,.npz 格式是更合适的选择。numpy.saveznumpy.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”)
“`

文件大小比较(示例):

你可以观察使用 savezsavez_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 vs savez_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.savenumpy.saveznumpy.savez_compressednumpy.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 语句管理资源。

通过合理地使用这些函数,你可以轻松地在程序运行之间保存计算状态,共享数据集,或者处理超出内存容量的大型数组。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部