numpy.save 函数详解:Python NumPy 数组保存指南 – wiki基地


NumPy save 函数详解:Python NumPy 数组保存指南

引言

在数据科学、机器学习、科学计算等领域,NumPy (Numerical Python) 是 Python 生态系统中不可或缺的基石。它提供了强大的 N 维数组对象(ndarray)以及一系列用于处理这些数组的函数。当我们处理大型数据集、进行复杂的计算或训练模型时,经常需要将 NumPy 数组的状态持久化保存到磁盘上。这不仅是为了方便后续加载和使用,也是为了在不同程序、不同运行环境之间共享数据,或者保存计算的中间结果以避免重复计算。

NumPy 提供了多种保存数组数据的方法,其中 numpy.save 函数是最基础也是最常用的方法之一,专门用于将单个 NumPy 数组以 NumPy 原生的二进制格式(.npy 文件)保存到磁盘。这种格式高效、紧凑,并且能够完整地保留数组的形状(shape)、数据类型(dtype)以及数据本身。

本文将深入探讨 numpy.save 函数的方方面面,包括其基本用法、参数详解、.npy 文件格式的内部机制、与其他保存方法的比较、安全性考量以及最佳实践。无论您是 NumPy 的初学者还是有经验的用户,希望本文都能为您提供一个关于如何有效保存 NumPy 数组的全面指南。

numpy.save 函数基础

核心功能

numpy.save 函数的核心目标非常明确:将一个 NumPy 数组对象序列化并写入到一个二进制文件中。这个文件通常使用 .npy 作为扩展名,这是一种 NumPy 自定义的、用于存储单个数组的标准格式。

基本语法

numpy.save 函数的签名如下:

python
numpy.save(file, arr, allow_pickle=True, fix_imports=True)

  • file: 指定要保存的文件路径(字符串)或一个支持 .write() 方法的文件对象(file-like object)。如果提供的是文件路径字符串,NumPy 会自动打开(或创建)该文件进行写入,并在完成后关闭它。如果路径中不包含扩展名,NumPy 不会 自动添加 .npy,但强烈建议手动添加 .npy 后缀以符合约定,方便识别和加载。
  • arr: 需要保存的 NumPy 数组。它可以是任何 NumPy 数组对象(ndarray),或者可以被 numpy.array() 转换的对象(array-like)。
  • allow_pickle (可选, 默认为 True): 一个布尔值,决定是否允许使用 Python 的 pickle 模块来保存包含 Python 对象(非基本 NumPy 数据类型)的数组。这是一个重要的安全参数,我们将在后面详细讨论。
  • fix_imports (可选, 默认为 True): 主要用于在 Python 3 环境下保存数据,以便能在 Python 2 环境下加载。随着 Python 2 的淘汰,这个参数的实际意义逐渐减弱,但在需要跨版本兼容的遗留场景下可能仍有用。

简单示例

让我们看一个最简单的例子:创建一个 NumPy 数组并将其保存到文件中。

“`python
import numpy as np

创建一个简单的 NumPy 数组

my_array = np.arange(12).reshape(3, 4)
print(“原始数组:”)
print(my_array)
print(“数组形状:”, my_array.shape)
print(“数据类型:”, my_array.dtype)

使用 numpy.save 保存数组到文件 ‘my_array.npy’

file_path = ‘my_array.npy’
np.save(file_path, my_array)

print(f”\n数组已成功保存到 ‘{file_path}'”)

后续可以使用 numpy.load 加载回来

loaded_array = np.load(file_path)
print(“\n加载回来的数组:”)
print(loaded_array)

验证加载的数据是否与原始数据一致

assert np.array_equal(my_array, loaded_array)
print(“\n加载的数组与原始数组一致!”)
“`

在这个例子中,我们创建了一个 3×4 的整数数组,并使用 np.save 将其保存到名为 my_array.npy 的文件中。随后,我们使用 np.load 成功地将数据加载回来,并验证了其与原始数组完全相同。

参数详解

file 参数

file 参数的灵活性是 numpy.save 的一个优点。

  1. 字符串路径: 这是最常见的用法。你提供一个包含文件名的字符串,例如 'data/my_array.npy'/path/to/your/data.npy。NumPy 会处理文件的打开、写入和关闭。如果文件已存在,它将被覆盖。如果目录不存在,会抛出 FileNotFoundError
  2. 文件对象: 你可以传入一个已经打开的、处于二进制写入模式('wb')的文件对象。这在你需要更精细地控制文件句柄(例如,在 with 语句中管理)或者将 NumPy 数组写入到非标准文件目标(如内存流 io.BytesIO)时非常有用。

“`python
import numpy as np
import io

示例:保存到文件对象 (磁盘文件)

arr_float = np.random.rand(5, 5)
with open(‘float_array.npy’, ‘wb’) as f:
np.save(f, arr_float)
print(“数组已通过文件对象保存到 ‘float_array.npy'”)

示例:保存到内存中的 BytesIO 对象

arr_complex = np.array([1+2j, 3+4j, 5+6j], dtype=np.complex128)
mem_file = io.BytesIO()
np.save(mem_file, arr_complex)
print(“数组已保存到内存 BytesIO 对象”)

可以从 BytesIO 对象中加载回来

mem_file.seek(0) # 重要:将指针移回文件开头
loaded_from_mem = np.load(mem_file)
assert np.array_equal(arr_complex, loaded_from_mem)
print(“从 BytesIO 加载的数组与原始复数数组一致!”)
“`

关于 .npy 扩展名: 虽然 numpy.save 函数本身不强制要求文件扩展名为 .npy,但这是 NumPy 社区广泛遵循的约定。使用 .npy 可以清晰地表明该文件包含一个以 NumPy 特定二进制格式存储的数组,方便人类用户和其他工具识别。numpy.load 函数在加载时也不依赖扩展名,它会读取文件头来确定格式。然而,坚持使用 .npy 是一个良好的实践。

arr 参数

这个参数就是要保存的数组。它必须是一个 NumPy ndarray 实例,或者是可以被 NumPy 隐式转换成数组的对象,例如 Python 列表或元组。

“`python
import numpy as np

可以直接保存 Python 列表,NumPy 会先将其转换为数组

python_list = [[1, 2], [3, 4]]
np.save(‘from_list.npy’, python_list)

加载回来会得到一个 NumPy 数组

loaded_arr = np.load(‘from_list.npy’)
print(f”从列表保存并加载回来的类型: {type(loaded_arr)}”)
print(loaded_arr)
“`

allow_pickle 参数:安全性和对象数组

allow_pickle 是一个至关重要的参数,尤其涉及到安全性和处理包含非数值类型(如 Python 对象)的数组时。

  • 默认行为 (allow_pickle=True): 当数组的数据类型 dtypeobject 时,意味着数组的元素可以是任意 Python 对象(字符串、字典、自定义类的实例等)。在这种情况下,如果 allow_pickle=Truenumpy.save 会使用 Python 的 pickle 模块来序列化这些对象。
  • Pickle 的风险: pickle 是一个强大的序列化工具,但它存在安全风险。加载一个包含恶意构造的 pickle 数据的文件可能导致执行任意代码。因此,绝对不要加载来自不受信任来源的、且允许 pickle 的 NumPy 文件(无论是 .npy 还是 .npz
  • 禁用 Pickle (allow_pickle=False): 如果将此参数设置为 Falsenumpy.save 在尝试保存 dtype=object 的数组时会检查是否需要 pickle。如果数组中包含需要 pickle 才能序列化的 Python 对象,它将抛出 ValueError,阻止保存。这是一种更安全的选择,可以防止意外地创建可能包含不安全 pickle 数据的文件。只有当对象数组中的所有元素本身都能被 NumPy 直接处理(这种情况很少见,通常还是基本类型),或者数组为空时,才不会报错。

“`python
import numpy as np

示例 1: 保存包含 Python 对象的数组 (默认 allow_pickle=True)

object_array = np.array([{‘a’: 1}, ‘hello’, None, [1, 2]], dtype=object)
try:
np.save(‘object_array_allowed.npy’, object_array, allow_pickle=True)
print(“对象数组 (allow_pickle=True) 保存成功。”)
# 加载时也需要 allow_pickle=True (并且要信任来源!)
loaded_obj_array = np.load(‘object_array_allowed.npy’, allow_pickle=True)
print(“对象数组加载成功:”, loaded_obj_array)
except Exception as e:
print(f”保存对象数组 (allow_pickle=True) 时出错: {e}”)

示例 2: 尝试保存对象数组,但禁用 pickle (allow_pickle=False)

try:
np.save(‘object_array_disallowed.npy’, object_array, allow_pickle=False)
print(“对象数组 (allow_pickle=False) 保存成功 (理论上不应发生,除非数组特殊)”)
except ValueError as e:
print(f”保存对象数组 (allow_pickle=False) 时按预期抛出 ValueError: {e}”) # 这通常是预期的行为

示例 3: 保存数值数组时,allow_pickle=False 是安全的且推荐的

numeric_array = np.linspace(0, 1, 10)
try:
np.save(‘numeric_array_safe.npy’, numeric_array, allow_pickle=False)
print(“数值数组 (allow_pickle=False) 保存成功。”)
# 加载时也可以指定 allow_pickle=False
loaded_numeric = np.load(‘numeric_array_safe.npy’, allow_pickle=False)
assert np.allclose(numeric_array, loaded_numeric)
print(“数值数组 (allow_pickle=False) 加载成功且数据一致。”)
except Exception as e:
print(f”保存或加载数值数组 (allow_pickle=False) 时出错: {e}”)
“`

最佳实践: 除非你完全理解风险并且绝对需要保存包含复杂 Python 对象的数组,否则强烈建议在调用 numpy.savenumpy.load 时始终设置 allow_pickle=False 对于标准的数值、布尔或固定长度字符串数组,这不会有任何问题,并且能提高安全性。

fix_imports 参数

这个参数主要是为了解决 Python 2 和 Python 3 之间 pickle 协议和模块路径可能存在的兼容性问题。当你在 Python 3 中保存数据,并且希望 Python 2 能够加载时,将 fix_imports=True(默认值)有助于确保 pickle 能够找到正确的模块。

然而,随着 Python 2 的生命周期结束 (EOL in 2020),绝大多数现代项目都运行在 Python 3 上。因此,在纯 Python 3 环境中,这个参数的影响通常可以忽略。如果你没有跨 Python 大版本加载数据的需求,可以不必关心它,保持默认值即可。

.npy 文件格式内部探秘

理解 .npy 文件的内部结构有助于更好地认识其效率和功能。一个 .npy 文件由两部分组成:

  1. 文件头 (Header):

    • 以一个固定的 6 字节魔术字符串 \x93NUMPY 开头,用于识别文件类型。
    • 接下来是 1 字节的主版本号和 1 字节的次版本号(例如 \x01\x00 代表 1.0 版本)。
    • 然后是 2 字节(对于 1.0 版本)或 4 字节(对于 2.0+ 版本)的无符号整数,表示头数据的长度。
    • 最后是头数据本身,它是一个 ASCII 编码的 Python 字典的字符串表示形式(使用 repr() 生成)。这个字典包含了描述数组所需的元数据,主要包括:
      • 'descr': 描述数组数据类型的字符串(例如 '<f8' 表示 little-endian double-precision float,'|b1' 表示 boolean)。
      • 'fortran_order': 一个布尔值,指示数组是 C 顺序(行主序,False)还是 Fortran 顺序(列主序,True)。
      • 'shape': 一个包含数组维度的元组(例如 (3, 4))。
    • 头数据的总长度被设计为能被 64 整除(为了对齐),不足的部分会用空格填充,并以一个换行符结束。
  2. 数组数据 (Data):

    • 紧跟在头数据之后的是数组的实际二进制数据。这些数据按照头信息中指定的 dtypeshapefortran_order(C 或 Fortran 顺序)连续存储。

这种设计的优点:

  • 高效: 读取时,只需解析文件头获取元数据,然后就可以直接将后续的二进制数据块读入内存(或进行内存映射),并根据元数据解释为 NumPy 数组。这比解析文本文件(如 CSV)快得多。
  • 紧凑: 对于数值类型,二进制表示通常比文本表示占用更少的空间。
  • 信息完整: 形状、数据类型和内存布局(C/Fortran order)都被完整保留,确保加载回来的数组与原始数组在结构上完全一致。
  • 自包含: .npy 文件包含了重建数组所需的所有信息,不依赖外部模式或描述。

加载数据:numpy.load 函数

numpy.save 的伴侣是 numpy.load。它的作用是从 .npy(或其他 NumPy 支持的格式如 .npz)文件中加载数组数据。

python
numpy.load(file, mmap_mode=None, allow_pickle=True, fix_imports=True, encoding='ASCII')

关键参数:

  • file: 要加载的文件路径或文件对象(需要以二进制读取模式 'rb' 打开)。
  • mmap_mode (可选): 如果设置为 'r+' (读写) 或 'c' (写时复制),numpy.load 会使用内存映射(memory mapping)来加载数组。这意味着数组数据不会立即全部读入内存,而是直接映射到文件在磁盘上的内容。这对于处理远超可用内存的大型数组非常有用。修改内存映射数组(如果模式允许)会直接反映到磁盘文件中。默认 None 表示将整个数组加载到内存中。
  • allow_pickle (可选, 默认为 True): 与 numpy.save 中的同名参数对应。加载时,如果文件头表明数据可能包含 pickle 对象(通常是因为保存时 dtype=objectallow_pickle=True),并且此参数为 True,则会尝试使用 pickle 反序列化。同样存在安全风险,对于不受信任的文件,务必设置为 False。如果设置为 False 但文件确实包含 pickle 数据,会抛出 ValueError
  • fix_imports, encoding: 主要用于处理 Python 2/3 兼容性和 pickle 加载时的编码问题。在现代 Python 3 环境下通常使用默认值即可。

示例:

“`python
import numpy as np

假设 ‘my_array.npy’ 是之前用 np.save 保存的文件

file_path = ‘my_array.npy’

基本加载

loaded_arr = np.load(file_path)
print(“基本加载:”, loaded_arr.shape, loaded_arr.dtype)

安全加载 (推荐用于任何来源的文件,尤其是数值数据)

try:
safe_loaded_arr = np.load(file_path, allow_pickle=False)
print(“安全加载 (allow_pickle=False):”, safe_loaded_arr.shape, safe_loaded_arr.dtype)
assert np.array_equal(loaded_arr, safe_loaded_arr)
except ValueError as e:
print(f”尝试安全加载时出错 (可能是对象数组?): {e}”)

内存映射加载 (适用于大文件)

注意:需要文件确实存在且较大才能体现优势

mmap_loaded_arr = np.load(file_path, mmap_mode=’r’)

print(“内存映射加载:”, mmap_loaded_arr.shape, mmap_loaded_arr.dtype)

# 使用 mmap_loaded_arr 时,数据可能按需从磁盘读取

print(“内存映射数组的第一个元素:”, mmap_loaded_arr[0, 0])

del mmap_loaded_arr # 最好显式删除或确保文件关闭以释放映射

“`

与其他 NumPy 保存方法的比较

NumPy 提供了多种保存数组的方法,了解它们的区别有助于选择最适合你需求的工具:

  1. numpy.save(file, arr):

    • 用途: 保存单个数组到二进制 .npy 文件。
    • 优点: 速度快,存储空间相对高效(对数值类型),完整保留数组元数据,NumPy 原生格式。
    • 缺点: 每个文件只能存一个数组,二进制格式不易于人类阅读或被非 NumPy 工具直接使用。
  2. numpy.savez(file, *args, **kwds):

    • 用途: 将多个数组保存到一个未压缩.npz 文件中。.npz 文件本质上是一个 zip 归档,其中每个数组存储为一个 .npy 文件(以传入的关键字参数名或默认名 arr_0, arr_1, … 命名)。
    • 优点: 方便地将多个相关数组打包在一个文件中。加载时返回一个类似字典的对象,可以通过键名访问各数组。
    • 缺点: 未压缩,对于大型数组可能占用较多空间。
  3. numpy.savez_compressed(file, *args, **kwds):

    • 用途: 将多个数组保存到一个压缩.npz 文件中。功能与 numpy.savez 类似,但使用 zip 压缩。
    • 优点: 节省磁盘空间,尤其对于内容有冗余的数组效果显著。方便打包多个数组。
    • 缺点: 保存和加载过程需要额外的压缩/解压缩时间,CPU 开销比 savez 稍高。
  4. numpy.savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n', header='', footer='', comments='# ', encoding=None):

    • 用途: 将一维或二维数组保存为文本文件(如 CSV, TSV)。
    • 优点: 文件是人类可读的,易于被电子表格软件、其他编程语言或文本处理工具读取。可以自定义格式、分隔符、添加头/尾信息。
    • 缺点: 速度慢得多,占用空间大得多,可能损失精度(取决于 fmt 参数),只能处理 1D/2D 数组,无法保存 dtypeshape 之外的元数据(如 Fortran order)。不适合非常大的数组。

选择指南:

  • 需要保存单个数组,追求速度和效率,且主要在 NumPy 环境中使用? -> numpy.save
  • 需要保存多个相关数组到一个文件,不关心压缩? -> numpy.savez
  • 需要保存多个相关数组到一个文件,且希望节省磁盘空间? -> numpy.savez_compressed
  • 需要将数组数据导出为人类可读的格式,或与其他非 NumPy 工具(如电子表格)共享,且数组是 1D 或 2D,对性能和精度要求不高? -> numpy.savetxt
  • 需要处理非常巨大(TB 级别)的数组,或者需要更复杂的存储结构(如分层数据、元数据属性)? -> 考虑使用更专业的格式和库,如 HDF5 (配合 h5pytables 库)。

性能考量

  • 二进制 vs 文本: numpy.save 使用的二进制 .npy 格式通常比 numpy.savetxt 生成的文本格式在读写速度和存储空间上都有显著优势。对于大型数值数组,差异可能达到数量级。
  • I/O 瓶颈: 保存和加载操作通常受磁盘 I/O 速度的限制。使用更快的存储设备(如 SSD)可以显著提升性能。
  • 内存映射 (numpy.loadmmap_mode): 对于远超内存容量的大型数组,使用内存映射可以避免一次性加载所有数据,从而能够处理这些数据,但访问速度会依赖于操作系统的页面缓存和磁盘性能。
  • 压缩 (savez_compressed): 压缩可以减少磁盘空间和 I/O 传输量,但会增加 CPU 计算时间。对于 I/O 瓶颈显著或网络传输成本高昂的场景,压缩可能反而提升整体效率。

最佳实践和注意事项

  1. 使用 .npy 扩展名: 坚持为 numpy.save 生成的文件使用 .npy 后缀。
  2. 优先 allow_pickle=False: 为了安全,除非明确需要且来源可信,否则在 saveload 时都设置 allow_pickle=False
  3. 选择合适的保存函数: 根据需求(单个/多个数组,压缩,文本/二进制)选择 save, savez, savez_compressed, 或 savetxt
  4. 管理文件路径: 使用 ospathlib 模块来构建和管理文件路径,确保跨平台兼容性。
  5. 错误处理: 使用 try...except 块来捕获可能发生的错误,如 FileNotFoundError(路径无效)、PermissionError(无写入权限)、ValueError(例如 allow_pickle=False 时尝试保存对象数组)。
  6. 文档化: 记录你保存的 .npy 文件内容、来源以及生成方式,尤其是在项目中共享数据时。
  7. 版本控制: 将生成数据的代码纳入版本控制。对于较小的数据文件,有时也可以直接将其纳入版本控制(如 Git LFS),但对于大型数据,通常只管理生成脚本。
  8. 数据类型和字节序: .npy 格式会记录数据类型和字节序(endianness)。这使得 .npy 文件通常可以在不同架构的机器之间移植(例如 little-endian 的 x86 和 big-endian 的某些旧架构),NumPy 会在加载时处理字节序转换。

结语

numpy.save 是 NumPy 库中用于持久化单个数组的核心工具。它提供了一种高效、可靠的方式来将数组数据以原生的二进制 .npy 格式存储到磁盘,完整保留了数组的结构和内容。通过理解其工作原理、参数(特别是 allow_pickle 的安全含义)以及与其他保存方法的区别,开发者可以更加自信和有效地管理 NumPy 数据。

掌握 numpy.save 及其伴侣 numpy.load,是每一位使用 NumPy 进行数据处理和分析的 Python 程序员必备的基础技能。结合场景需求,合理选择 save, savez, savez_compressedsavetxt,并始终关注数据安全和性能考量,将使你的数据工作流更加健壮和高效。


发表评论

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

滚动至顶部