NumPy save vs savez:如何选择?一份深度对比与应用指南
引言:数据持久化的重要性
在数据科学、机器学习、科学计算以及其他许多依赖于大规模数值计算的领域,NumPy 库因其高效的多维数组处理能力而成为事实上的标准。然而,数据的生命周期不仅仅局限于内存中的计算。为了实现数据的复用、模型的保存与加载、计算结果的分享或者避免重复耗时的预处理,我们需要将内存中的 NumPy 数组存储到磁盘上,并在需要时重新加载。这个过程被称为数据持久化(Data Persistence)。
NumPy 提供了多种内置的方法来实现数组的持久化,其中最常用和推荐的是 numpy.save
和 numpy.savez
(以及 numpy.savez_compressed
) 函数。它们都旨在以高效的二进制格式存储 NumPy 数组,从而保留数组的数据类型、形状和内容,避免了文本格式(如CSV)可能带来的精度损失和读写效率低下问题。
虽然 save
和 savez
都服务于数据持久化的目的,但它们之间存在着根本性的设计差异,这些差异决定了它们各自适用的场景。理解这些区别对于优化数据存储、提高程序效率和简化数据管理至关重要。
本文将深入探讨 numpy.save
、numpy.savez
以及 numpy.savez_compressed
的工作原理、使用方法、优缺点以及适用场景。通过详细的对比分析,我们将帮助您在不同的数据存储需求下,明智地选择最合适的 NumPy 持久化工具。
NumPy 的二进制存储格式:.npy 与 .npz
在深入函数细节之前,了解一下 NumPy 使用的两种主要的二进制文件格式非常有帮助:
-
.npy 格式: 这是
numpy.save
函数创建的标准格式。它专门用于存储单个 NumPy 数组。.npy
文件包含一个文件头,记录了数组的形状(shape)、数据类型(dtype)、字节顺序(endianness)以及其他元信息,紧接着是数组的原始二进制数据。这种格式非常紧凑和高效,可以直接映射到内存中进行读取(memory-mapping),对于加载大型单数组尤其有利。 -
.npz 格式: 这是
numpy.savez
和numpy.savez_compressed
函数创建的标准格式。它实际上是一个 ZIP 压缩包,但其内部包含的是一个或多个.npy
文件。每个.npy
文件对应着一个被保存的数组。.npz
格式允许我们将多个相关的 NumPy 数组打包到一个文件中,方便管理和分发。savez
创建的是未压缩的 ZIP 存档,而savez_compressed
创建的是使用 zlib 压缩的 ZIP 存档,可以显著减小文件大小。
理解这两种底层格式是理解 save
和 savez
函数差异的关键。save
处理单个 .npy
文件,而 savez
和 savez_compressed
处理包含多个 .npy
文件的 .npz
压缩包。
深度解析 numpy.save
numpy.save
函数是 NumPy 中用于将单个数组以 .npy
格式保存到文件的基本工具。
基本语法:
python
numpy.save(file, arr, allow_pickle=True, fix_imports=True)
file
: 文件名(字符串)或文件对象。如果提供文件名,会自动添加.npy
扩展名(如果文件名没有指定扩展名的话)。arr
: 要保存的 NumPy 数组。allow_pickle
: (布尔值,默认为 True)是否允许使用 Python pickle 协议保存非 NumPy 对象数组。通常建议将其设置为False
,除非您确定需要保存包含任意 Python 对象的数组,并且理解潜在的安全风险(加载 pickle 文件可能执行恶意代码)。对于标准的数值 NumPy 数组,此参数通常不影响。fix_imports
: (布尔值,默认为 True)与 pickle 协议相关,帮助在不同 Python 版本之间加载旧的 pickle 文件。对于.npy
格式的纯 NumPy 数组,通常不重要。
工作原理:
numpy.save
会创建一个 .npy
文件。它首先将数组的形状、数据类型、是否 Fortran 连续等信息写入文件头部,然后将数组的原始字节数据直接写入文件头部之后。整个过程非常直接和高效。
优点:
- 简单易用: 语法非常简洁,只需指定文件名和要保存的数组。
- 高效快速: 对于保存单个数组而言,
save
的速度通常是最快的,因为它涉及到最少的开销——直接的二进制写入。 - 节省空间(对于单个数组):
.npy
格式是为单个数组优化的,不包含压缩包的额外开销(如目录结构)。 - 支持内存映射(Memory-Mapping): 使用
numpy.load
加载.npy
文件时,可以通过设置mmap_mode
参数启用内存映射。这意味着数据不是一次性全部加载到内存,而是根据需要从文件中读取。这对于处理超出可用内存的大型数组非常有用。
缺点:
- 只能保存单个数组: 这是
save
最主要的限制。如果您有多个相关的数组需要保存,必须为每个数组调用一次save
,导致生成多个文件。这会使得文件管理变得复杂,尤其当数组数量很多时。
示例代码:
“`python
import numpy as np
创建一个 NumPy 数组
data_array = np.random.rand(100, 50)
print(f”要保存的数组形状: {data_array.shape}, 数据类型: {data_array.dtype}”)
使用 save 保存数组到文件
file_name_save = ‘single_array.npy’
np.save(file_name_save, data_array)
print(f”数组已保存到文件: {file_name_save}”)
加载保存的数组
loaded_array_save = np.load(file_name_save)
print(f”从文件 {file_name_save} 加载的数组形状: {loaded_array_save.shape}”)
print(f”数据是否一致: {np.array_equal(data_array, loaded_array_save)}”)
示例:保存到文件对象
with open(‘single_array_obj.npy’, ‘wb’) as f:
np.save(f, data_array)
print(“数组已保存到文件对象: single_array_obj.npy”)
loaded_array_obj = np.load(‘single_array_obj.npy’)
print(f”从文件对象加载的数组形状: {loaded_array_obj.shape}”)
print(f”数据是否一致: {np.array_equal(data_array, loaded_array_obj)}”)
示例:使用 mmap_mode 加载大型文件(假设文件很大)
loaded_array_mmap = np.load(file_name_save, mmap_mode=’r’)
print(f”使用 mmap_mode 加载的数组形状: {loaded_array_mmap.shape}”)
print(“这是一个内存映射对象,数据按需加载”)
注意:使用 mmap_mode=’r’ 时,修改 loaded_array_mmap 不会写入文件。
如果需要写入,可以使用 ‘r+’ 或 ‘w+’/’c’ (需谨慎)
一旦完成 mmap 操作,通常不需要显式关闭文件,但如果使用 ‘w+’/’c’ 可能需要flush/close。
“`
深度解析 numpy.savez
numpy.savez
函数用于将多个NumPy 数组保存到一个.npz
文件中。这个 .npz
文件是一个未压缩的 ZIP 存档。
基本语法:
python
numpy.savez(file, *args, **kwds, allow_pickle=True, fix_imports=True)
file
: 文件名(字符串)或文件对象。如果提供文件名,会自动添加.npz
扩展名(如果文件名没有指定扩展名的话)。*args
: 要保存的数组,作为位置参数传递。这些数组将被保存到.npz
文件中,并自动命名为arr_0
,arr_1
,arr_2
, …**kwds
: 要保存的数组,作为关键字参数传递。数组的关键字名称将作为它们在.npz
文件中的名称。使用关键字参数是推荐的方式,因为它为每个保存的数组提供了清晰、有意义的名称。allow_pickle
: (布尔值,默认为 True)与save
相同,控制是否允许保存非 NumPy 对象数组。fix_imports
: (布尔值,默认为 True)与save
相同,与 pickle 协议相关。
工作原理:
numpy.savez
会创建一个 ZIP 存档文件(.npz
文件)。对于每个作为参数传递的数组,它会将其序列化为一个单独的 .npy
文件,并将这个 .npy
文件添加到 ZIP 存档中。如果使用关键字参数(例如 my_array=arr
),那么在 ZIP 存档中对应的 .npy
文件将命名为 my_array.npy
。如果使用位置参数(例如 arr1
, arr2
),它们将分别被保存为 arr_0.npy
, arr_1.npy
等。
优点:
- 保存多个数组: 核心优势,可以将多个相关的数组存储在一个文件中,便于组织和管理。
- 使用关键字参数命名: 允许为保存的数组指定有意义的名称,提高代码可读性,并方便加载时按名称访问。
- 方便分发: 多个相关数据集可以打包成一个文件进行分享。
缺点:
- 文件大小可能较大:
savez
创建的是未压缩的 ZIP 存档。虽然避免了压缩/解压缩的计算开销,但最终文件大小可能会比单独保存每个数组再手动压缩更大(因为 ZIP 格式本身的开销)。 - 加载方式不同: 使用
numpy.load
加载.npz
文件时,返回的是一个 NpzFile 对象(一个类似字典的对象),需要通过键(即保存时使用的名称)来访问具体的数组。这比加载单个.npy
文件多一步操作。 - 不支持内存映射(对于 NpzFile 对象整体):
numpy.load
加载.npz
文件得到的 NpzFile 对象本身不支持内存映射。虽然 NpzFile 对象内部的每个数组(在被访问时)可以看作是从 ZIP 存档中提取并加载到内存的.npy
数据,但无法像加载单个.npy
文件那样直接对整个.npz
文件进行内存映射级别的操作。加载特定键对应的数组时,该数组会被完全加载到内存。
示例代码:
“`python
import numpy as np
创建多个 NumPy 数组
array_a = np.array([1, 2, 3, 4, 5])
array_b = np.arange(10).reshape(2, 5)
array_c = np.random.rand(3, 3)
print(f”要保存的数组 A 形状: {array_a.shape}”)
print(f”要保存的数组 B 形状: {array_b.shape}”)
print(f”要保存的数组 C 形状: {array_c.shape}”)
使用 savez 保存多个数组到文件 (使用关键字参数)
file_name_savez_kw = ‘multiple_arrays_kw.npz’
np.savez(file_name_savez_kw, arr_a=array_a, matrix_b=array_b, random_c=array_c)
print(f”多个数组已保存到文件: {file_name_savez_kw} (使用关键字)”)
使用 savez 保存多个数组到文件 (使用位置参数)
file_name_savez_pos = ‘multiple_arrays_pos.npz’
np.savez(file_name_savez_pos, array_a, array_b, array_c)
print(f”多个数组已保存到文件: {file_name_savez_pos} (使用位置参数)”)
加载使用关键字参数保存的 npz 文件
loaded_data_kw = np.load(file_name_savez_kw)
查看 npz 文件中包含的数组名称 (键)
print(f”文件 {file_name_savez_kw} 中的键: {list(loaded_data_kw.keys())}”)
通过键访问具体的数组
loaded_array_a_kw = loaded_data_kw[‘arr_a’]
loaded_array_b_kw = loaded_data_kw[‘matrix_b’]
loaded_array_c_kw = loaded_data_kw[‘random_c’]
print(f”从 {file_name_savez_kw} 加载 arr_a 的形状: {loaded_array_a_kw.shape}”)
print(f”从 {file_name_savez_kw} 加载 matrix_b 的形状: {loaded_array_b_kw.shape}”)
print(f”从 {file_name_savez_kw} 加载 random_c 的形状: {loaded_array_c_kw.shape}”)
print(f”数组 A 是否一致: {np.array_equal(array_a, loaded_array_a_kw)}”)
print(f”数组 B 是否一致: {np.array_equal(array_b, loaded_array_b_kw)}”)
print(f”数组 C 是否一致: {np.array_equal(array_c, loaded_array_c_kw)}”)
重要:加载 .npz 文件后,返回的是一个类似文件句柄的对象,使用完毕后应关闭以释放资源
loaded_data_kw.close()
print(f”已关闭文件 {file_name_savez_kw}”)
加载使用位置参数保存的 npz 文件
loaded_data_pos = np.load(file_name_savez_pos)
查看 npz 文件中包含的数组名称 (键) – 自动命名
print(f”文件 {file_name_savez_pos} 中的键 (自动命名): {list(loaded_data_pos.keys())}”)
通过自动生成的键访问数组
loaded_array_a_pos = loaded_data_pos[‘arr_0’]
loaded_array_b_pos = loaded_data_pos[‘arr_1’]
loaded_array_c_pos = loaded_data_pos[‘arr_2’]
print(f”从 {file_name_savez_pos} 加载 arr_0 的形状: {loaded_array_a_pos.shape}”)
print(f”数组 A 是否一致 (从位置参数加载): {np.array_equal(array_a, loaded_array_a_pos)}”)
关闭文件句柄
loaded_data_pos.close()
print(f”已关闭文件 {file_name_savez_pos}”)
“`
介绍 numpy.savez_compressed
numpy.savez_compressed
是 numpy.savez
的一个变种,它也用于将多个数组保存到一个 .npz
文件中,但主要的区别在于它使用了 zlib 压缩来减小文件大小。
基本语法:
python
numpy.savez_compressed(file, *args, **kwds, allow_pickle=True, fix_imports=True)
语法与 savez
完全相同。
工作原理:
与 savez
类似,它创建一个 ZIP 存档,并将每个数组序列化为内部的 .npy
文件。但是,在将这些 .npy
文件添加到 ZIP 存档时,它会应用 zlib 压缩。
优点:
- 保存多个数组: 继承了
savez
的多数组保存能力。 - 显著减小文件大小: 对于包含许多重复值、规则模式或浮点数组(尤其是精度要求不是极高时)的数据,压缩可以极大地减少磁盘空间占用。这对于存储和传输大量数据非常有利。
- 使用关键字参数命名: 同样支持为数组指定有意义的名称。
缺点:
- 保存和加载速度较慢: 压缩和解压缩过程需要额外的计算,因此与
save
和savez
相比,savez_compressed
在保存和加载时会花费更多的时间。这是一个典型的空间换时间的权衡。 - 加载方式相同: 加载时同样返回 NpzFile 对象,需要按键访问,且不支持对整个
.npz
文件的内存映射。
示例代码:
“`python
import numpy as np
import os
import time
创建一些数组,包含一些容易压缩的数据(例如,有很多零或重复值)
array_large_zeros = np.zeros((1000, 1000))
array_large_rand = np.random.rand(1000, 1000)
array_mixed = np.hstack((array_large_zeros, array_large_rand))
print(f”大型零数组形状: {array_large_zeros.shape}”)
print(f”大型随机数组形状: {array_large_rand.shape}”)
print(f”混合数组形状: {array_mixed.shape}”)
file_name_savez = ‘large_arrays_uncompressed.npz’
file_name_savez_compressed = ‘large_arrays_compressed.npz’
使用 savez 保存 (未压缩)
start_time = time.time()
np.savez(file_name_savez, zeros=array_large_zeros, rand=array_large_rand, mixed=array_mixed)
end_time = time.time()
size_uncompressed = os.path.getsize(file_name_savez)
print(f”使用 savez 保存耗时: {end_time – start_time:.4f} 秒”)
print(f”savez 文件大小: {size_uncompressed / (1024*1024):.2f} MB”)
使用 savez_compressed 保存 (压缩)
start_time = time.time()
np.savez_compressed(file_name_savez_compressed, zeros=array_large_zeros, rand=array_large_rand, mixed=array_mixed)
end_time = time.time()
size_compressed = os.path.getsize(file_name_savez_compressed)
print(f”使用 savez_compressed 保存耗时: {end_time – start_time:.4f} 秒”)
print(f”savez_compressed 文件大小: {size_compressed / (1024*1024):.2f} MB”)
print(f”压缩节省空间比例: {(1 – size_compressed / size_uncompressed):.2%}”)
模拟加载速度差异
加载未压缩
start_time = time.time()
loaded_data_uncompressed = np.load(file_name_savez)
loaded_zeros_uncompressed = loaded_data_uncompressed[‘zeros’]
loaded_rand_uncompressed = loaded_data_uncompressed[‘rand’]
loaded_mixed_uncompressed = loaded_data_uncompressed[‘mixed’]
end_time = time.time()
print(f”使用 savez 加载耗时: {end_time – start_time:.4f} 秒”)
loaded_data_uncompressed.close()
加载压缩
start_time = time.time()
loaded_data_compressed = np.load(file_name_savez_compressed)
loaded_zeros_compressed = loaded_data_compressed[‘zeros’]
loaded_rand_compressed = loaded_data_compressed[‘rand’]
loaded_mixed_compressed = loaded_data_compressed[‘mixed’]
end_time = time.time()
print(f”使用 savez_compressed 加载耗时: {end_time – start_time:.4f} 秒”)
loaded_data_compressed.close()
清理文件
os.remove(file_name_savez)
os.remove(file_name_savez_compressed)
``
savez_compressed
通过上面的示例,您会看到在文件大小上的巨大优势,尤其对于包含大量重复数据(如
array_large_zeros`)的场景。但同时,其保存和加载时间会相对更长。
NumPy Load:加载保存的数据
所有使用 numpy.save
, numpy.savez
, numpy.savez_compressed
保存的文件,都统一使用 numpy.load
函数来加载。numpy.load
会根据文件的格式自动识别是 .npy
还是 .npz
,并采取相应的加载机制。
基本语法:
python
numpy.load(file, mmap_mode=None, allow_pickle=False, fix_imports=True, encoding='ASCII')
file
: 文件名(字符串)或文件对象。mmap_mode
: (字符串,可选)仅对.npy
文件有效。如果设置为'r'
,'r+'
,'w+'
,'c'
,会启用内存映射。allow_pickle
: (布尔值,默认为 False)是否允许加载使用 pickle 协议保存的对象。出于安全原因,默认为False
是推荐设置。如果加载一个包含 pickle 数据的.npy
或.npz
文件时此参数为False
,将会引发ValueError
。fix_imports
: (布尔值,默认为 True)与 pickle 相关。encoding
: (字符串,默认为 ‘ASCII’)加载 Python 2 pickle 文件时使用的编码。
加载 .npy 文件: np.load('my_array.npy')
会直接返回保存的那个 NumPy 数组。
加载 .npz 文件: np.load('my_arrays.npz')
会返回一个 NpzFile 对象。这是一个类似字典的对象,可以通过方括号 []
和键名来访问其中保存的各个数组。例如 data['array_name']
。重要提示: NpzFile 对象在访问具体数组时才会真正从文件中读取数据。一旦您完成了对数据的读取,应该调用 NpzFile 对象的 .close()
方法来释放文件资源,尤其是在循环中处理大量 .npz
文件时,不关闭文件句柄可能导致资源耗尽或文件锁问题。在 with 语句中使用 np.load
可以确保自动关闭,这是一个推荐的模式。
示例:加载并关闭 .npz 文件
“`python
import numpy as np
假设已经有了 multiple_arrays_kw.npz 文件
推荐使用 with 语句加载 .npz 文件
try:
with np.load(‘multiple_arrays_kw.npz’) as loaded_data:
print(f”文件中的键: {list(loaded_data.keys())}”)
# 访问数组
array_a = loaded_data[‘arr_a’]
array_b = loaded_data[‘matrix_b’]
print(f”加载的 array_a: {array_a}”)
print(f”加载的 array_b 形状: {array_b.shape}”)
# with 块结束,文件已自动关闭
print(“文件已自动关闭 (使用 with 语句)”)
# 如果不使用 with 语句,需要手动关闭
loaded_data_manual = np.load('multiple_arrays_kw.npz')
print(f"手动加载文件中的键: {list(loaded_data_manual.keys())}")
# 访问数组
# ... 使用 loaded_data_manual ...
loaded_data_manual.close()
print("文件已手动关闭 (.close())")
except FileNotFoundError:
print(“文件未找到,请先运行保存示例”)
“`
NumPy save vs savez:如何选择?综合对比与决策指南
经过上面的详细分析,我们可以总结 numpy.save
和 numpy.savez
(包括 savez_compressed
) 之间的核心差异和适用场景,并构建一个决策流程。
核心差异概览:
特性 | numpy.save |
numpy.savez |
numpy.savez_compressed |
---|---|---|---|
保存对象数量 | 单个 NumPy 数组 | 多个 NumPy 数组 | 多个 NumPy 数组 |
文件格式 | .npy |
.npz (未压缩 ZIP 存档) |
.npz (zlib 压缩 ZIP 存档) |
文件结构 | 文件头 + 单个数组二进制数据 | ZIP 存档,包含多个 .npy 文件 |
压缩 ZIP 存档,包含多个 .npy 文件 |
命名方式 | 无需命名(文件名即标识) | 位置参数 (arr_0 , arr_1 …) 或 关键字参数 (自定义名称) |
位置参数 (arr_0 , arr_1 …) 或 关键字参数 (自定义名称) |
文件大小 | 适用于单数组,紧凑 | 通常大于等效的单个 .npy 文件总和,未压缩 |
通常小于等效的单个 .npy 文件总和,显著节省空间(取决于数据可压缩性) |
保存/加载速度 | 最快(直接二进制 I/O) | 较快(打包/解包有开销) | 较慢(包含压缩/解压缩计算) |
内存映射 | 支持 (numpy.load 的 mmap_mode ) |
不支持(对整个 .npz 文件) |
不支持(对整个 .npz 文件) |
加载返回类型 | NumPy 数组 | NpzFile 对象 (类似字典) | NpzFile 对象 (类似字典) |
文件管理 | 一个数组一个文件,可能导致文件碎片化 | 多个数组一个文件,便于组织 | 多个数组一个文件,便于组织 |
决策指南:
选择使用 save
、savez
还是 savez_compressed
,主要取决于以下几个因素:
-
您需要保存多少个数组?
- 只有一个数组: 优先考虑
numpy.save
。它简单、快速、高效,且支持内存映射,特别适合保存大型单数组。 - 有多个数组,且这些数组是相关的,希望打包在一起: 排除
numpy.save
,考虑numpy.savez
或numpy.savez_compressed
。
- 只有一个数组: 优先考虑
-
对文件大小是否有严格要求?或者数据是否具有良好的可压缩性?
- 文件大小不敏感,或者数据不易压缩(例如,纯随机浮点数): 可以使用
numpy.savez
。它比savez_compressed
保存和加载更快,同时提供了多数组打包的功能。 - 文件大小是重要考虑因素,需要最大限度地节省磁盘空间或降低传输带宽;或者数据包含大量重复值、零、规则模式等易压缩的特性: 选择
numpy.savez_compressed
。牺牲一定的速度换取显著的文件大小减少。
- 文件大小不敏感,或者数据不易压缩(例如,纯随机浮点数): 可以使用
-
对保存和加载速度有什么要求?
- 速度是首要考虑,尤其是需要频繁读写或处理实时数据: 如果是单个数组,用
save
。如果是多个数组,如果文件大小不是大问题,用savez
。savez_compressed
通常是最慢的。
- 速度是首要考虑,尤其是需要频繁读写或处理实时数据: 如果是单个数组,用
-
是否需要内存映射功能?
- 需要对一个非常大的数组进行内存映射,以避免一次性加载到内存: 只能使用
numpy.save
将其保存为.npy
文件,然后用numpy.load(..., mmap_mode=...)
加载。savez
和savez_compressed
不支持对整个.npz
文件进行内存映射。
- 需要对一个非常大的数组进行内存映射,以避免一次性加载到内存: 只能使用
-
文件管理的便利性?
- 希望将一组相关的数组作为一个整体进行管理、存储或传输: 使用
savez
或savez_compressed
。一个.npz
文件比多个.npy
文件更容易管理。
- 希望将一组相关的数组作为一个整体进行管理、存储或传输: 使用
总结的决策树:
您有多少个NumPy数组需要保存?
├── 单个数组
│ └──> 使用 numpy.save
│ (优点: 简单, 快速, .npy文件, 支持mmap)
│
└── 多个数组
└──> 这些数组是否需要打包到一个文件? (通常是的)
└──> 是否需要极致地减小文件大小? (数据易压缩,或存储/传输受限)
├── 是
│ └──> 使用 numpy.savez_compressed
│ (优点: 保存多个数组到一个.npz, 显著减小文件大小)
│ (缺点: 保存/加载速度较慢)
│
└── 否 (文件大小不太敏感,或速度更优先)
└──> 使用 numpy.savez
(优点: 保存多个数组到一个.npz, 保存/加载速度比compressed快)
(缺点: 文件大小通常比compressed大)
实际应用场景举例:
- 保存大型单张图像数据(例如科学成像、医学影像):
numpy.save
到.npy
文件,便于后续使用mmap_mode
高效加载进行处理。 - 保存机器学习模型的参数(权重、偏置),可能有多个层或组件的参数数组:
numpy.savez
或numpy.savez_compressed
。使用关键字参数为每个参数组命名(如weights_layer1
,bias_layer1
,weights_layer2
),方便加载时识别和应用。如果模型参数量巨大且磁盘空间紧张,可以考虑savez_compressed
。 - 保存科学模拟的多步结果: 每次模拟可能生成几个相关的数组(如当前状态、速度、温度等)。使用
numpy.savez
或savez_compressed
将同一时间步的所有结果保存在一个.npz
文件中,文件名可以包含时间信息,方便按时间步加载和分析。 - 保存实验数据,包含原始测量数据、处理后的特征、对应的标签等: 同样适合使用
savez
或savez_compressed
将这些相关的数组打包在一起,例如np.savez('experiment_001.npz', raw=raw_data, features=features, labels=labels)
。
allow_pickle=False
的重要性
在 save
, savez
, savez_compressed
以及 load
函数中都有 allow_pickle
参数,默认都是 True
(在较旧的 NumPy 版本中)。但从安全角度考虑,强烈建议在不需要保存/加载非 NumPy 对象的数组时,将 load
函数的 allow_pickle
参数明确设置为 False
。
NumPy 的 .npy
和 .npz
格式本身是二进制的,不涉及任意代码执行。然而,NumPy 允许在数组的 dtype
为 object
时,使用 Python 的 pickle 协议来序列化数组中的每个元素(这些元素可以是任意 Python 对象)。pickle 协议是强大的,但也存在安全风险:加载一个特制的 pickle 文件可能导致执行恶意代码。
如果您的数组元素不是 Python 对象(即数组的 dtype
不是 object
),而是标准的数值类型(如 int
, float
, bool
等),那么无论 allow_pickle
是 True
还是 False
,保存和加载的行为都是一样的,不会使用 pickle。
但是,如果您不确定文件的来源,或者文件可能由包含 object
dtype
数组的代码生成,将 np.load
的 allow_pickle
设置为 False
可以有效防止加载可能包含恶意 pickle 数据的对象数组。
从 NumPy 1.16.3 和 1.17.0 版本开始,np.load
的 allow_pickle
默认值被改为 False
,这是一个重要的安全改进。但在使用旧版本或为了代码向前兼容性,显式设置它是一个好习惯。保存时,如果数组 dtype
是 object
并且 allow_pickle=False
,则会抛出错误。
总结与结语
NumPy 的 save
和 savez
(以及 savez_compressed
) 是进行 NumPy 数组持久化的核心工具。它们各有优势,针对不同的数据存储需求提供了高效的解决方案。
numpy.save
: 单个数组的首选,简单、快速、支持内存映射,适用于保存大型独立数据集。numpy.savez
: 多个相关数组的容器,便于组织和管理,保存加载速度较快,适用于对文件大小不太敏感的场景。numpy.savez_compressed
: 多个相关数组的压缩容器,牺牲部分速度换取文件大小的显著减小,适用于存储空间或带宽有限、且数据可压缩性好的场景。
在实际应用中,根据您要保存的数组数量、对文件大小和速度的要求以及是否需要内存映射等因素,参考本文提供的对比分析和决策指南,选择最合适的函数。同时,注意在使用 numpy.load
加载 .npz
文件后关闭 NpzFile 对象,并在非必要时将 allow_pickle
设置为 False
以提高安全性。
掌握了 save
和 savez
的使用技巧,您将能更有效地管理和利用您的 NumPy 数据,为后续的数据分析、模型训练和结果复现打下坚实的基础。