Numpy 加载包含pickled数据的文件时出错
在Python中,pickling是将Python对象序列化为字节流的过程。如果您需要将一个对象存储在磁盘上,或者在网络上传输它,那么你必须将它pickling为一个字符串。在另一个程序中,你可以读取这个字符串并恢复原始对象。Numpy是一个基于Python的科学计算库,它经常处理包含pickle格式数据的文件,但是Numpy在从.npy文件中加载pickled数据时会遇到一些问题。
阅读更多:Numpy 教程
Numpy and Pickling
Numpy在处理数据时使用pickle。pickle是Python标准库中的一个模块,用于将Python对象转换为序列化的字节流,并在需要时将其反序列化为Python对象。使用pickle,您可以保存Python中的任何对象,包括自定义类,函数和方法,并在需要时重构为Python对象。Numpy继承了pickle,使用pickle将多维数组,数据类型和其他Numpy对象序列化为字节流。Numpy还通过将pickle格式和Numpy的二进制文件格式.npz和.npy混合使用,实现快速读写多维数据。虽然Numpy提供了一种有效的方法来处理pickle数据,但是从.npz和.npy文件中加载pickled数据时会出现一些奇怪的问题。
Numpy无法加载包含pickled数据的文件
当您尝试从.npz或.npy文件中加载包含pickled数据的文件时,您可能会遇到以下错误消息:
ValueError: Object arrays cannot be loaded when allow_pickle=False
这意味着什么?这有点不直观。您可能已经设置了np.load函数的allow_pickle参数为True,但是仍然遇到此错误。实际上,这个错误消息是非常有用的,因为它说明了您的错误。allow_pickle参数控制在加载二进制文件时是否允许pickle数据。设置这个参数为False会使Numpy直接加载二进制数据,而不进行任何pickle操作。这个错误消息说明您正在尝试加载一个不允许pickle的对象数组。这很疑惑,因为我们没有显式地要求Numpy将任何对象数组保存为pickle对象。
问题分析
我们要解释这个奇怪的问题,需要了解Numpy二进制文件的存储格式。当您创建一个包含Numpy对象的数组之后,您可能会将其保存到磁盘上,这样您就可以在以后的时间重新加载它。您可以使用.npz或.npy扩展名将Numpy数组保存到磁盘上。
.npz文件格式
Numpy压缩格式.npz是将一个或多个包含Numpy数组的npy格式文件压缩成一个单独的ZIP文件,保留每个数组的名称。npz文件包含至少一个.npy文件,并存储该文件的名称和数据。您可以使用np.savez方法将Numpy数组保存到.npz文件中以便以后加载。
.npy文件格式
.npy文件以二进制形式存储Numpy数组数据。这些文件包括一个-64字节的魔数字符串,后跟元数据头和数组数据。对于多维数组,元数据头包含维度的数量,每个维度的大小以及该数组的数据类型。例如,下面的表格显示了一个2D数组的.npy文件元数据头。
| Byte offset | C Type | 解释 |
|---|---|---|
| 0 | char[6] | 魔数(“.npy”) |
| 6 | uint8 | 主要版本号:NPY_MAJOR_VERSION |
| 7 | uint8 | 次要版本号:NPY_MINOR_VERSION| 8 | uint16 | 该数组的头部长度,以字节为单位 |
| 10 | uint8 | 该数组的数据类型 |
| 11 | uint32 | 该数组的维度数 |
| 15 | uint64 | 第一个维度的大小 |
| 23 | uint64 | 第二个维度的大小 |
| … | … | … |
值得注意的是,这里的第11个字节表示的是维度数,也就是描述该数组大小的整数列表(即,shape)。元数据头后的字节就是该数组的数据,顺序按照C语言的内存布局顺序存储。
回到我们的问题,那么为什么Numpy会在.npy文件中使用pickle数据呢?关键在于,如果一个Numpy数组不是C语言中的原始数据类型(如整数、浮点数、字符和布尔值),那么它需要作为Python对象来序列化。这会在Numpy压缩格式.npz中出现,因为它可以存储一个Python字典,其中每个值可以是一个Python对象。但在一个.npy文件中,所有的数组元素都必须是同一种C数据类型。因此,如果一个数组包含字符串、列表、元组或自定义类的元素等对象,它必须被pickling,才能被存储在.npy文件中。
解决方案
理解问题的原因是解决问题的一半。在我们的情况下,.npy文件中包含了一个不支持pickle的对象数组。为了解决这个问题,有以下两种方法:
1. 设置allow_pickle=True
在使用np.load函数加载一个包含pickle数据的.npy文件时,设置allow_pickle=True可以解决问题,因为这会告诉Numpy在加载数据时解开pickle数据。默认情况下,allow_pickle是关闭的。下面是以下代码示例:
import numpy as np
data = np.load('data.npy', allow_pickle=True)
但是,请注意,在开启allow_pickle选项时存在潜在的风险,例如加载可能包含恶意代码的pickle数据,因为pickle数据是可执行的Python代码。因此,应当只使用可信任来源的pickle数据。
2. 手动读取二进制文件
另一种方法是使用Python的内置模块pickle手动加载二进制文件并解pickle。通过这种方式,您可以选择在手动转换数据之前检查后pickle数据是否可信,以确保不会执行恶意代码。以下是一个代码示例:
import pickle
with open('data.npy', 'rb') as f:
# 跳过numpy格式的头信息
f.seek(128)
# 读取pickle数据
data = pickle.load(f, encoding='latin1')
在这个例子中,我们使用open函数打开.npy文件,并跳过了Numpy数组的头部信息。我们使用pickle模块手动处理两个步骤:首先反序列化pickle数据,并检查其是否可信。
总结
当尝试从一个包含pickle数据的根二进制文件中加载数据时,Numpy会遇到不能读取对象数组的问题。这种常见的错误信息告诉我们,该数组不支持pickle加载。为了解决这个问题,我们要注意.npy文件中对象数组的存在并使用allow_pickle=True或手动读取二进制文件的下面pickle模块来处理pickle数据。注意,pickle数据可能包含可执行代码,因此要确保其来源是可信任的。
极客教程