NEP 1 — NumPy 数组的简单文件格式#

作者:

Robert Kern <robert.kern@gmail.com>

状态:

最终

创建日期:

2007年12月20日

摘要#

我们提出一种标准的二进制文件格式(NPY),用于将单个任意NumPy数组持久化到磁盘上。该格式存储了重建数组所需的所有形状和dtype信息,即使在不同架构的机器上也能正确重建。该格式设计得尽可能简单,同时实现其有限的目标。该实现旨在纯粹使用Python,并作为主numpy包的一部分进行分发。

原理#

将NumPy数组保存到磁盘的轻量级、普遍存在的系统是一种常见需求。Python通常使用pickle [1] 将大多数Python对象保存到磁盘。这对于许多目的来说,与NumPy数组配合得很好,但它有一些缺点

  • 转储或加载pickle文件需要在内存中复制数据。对于大型数组来说,这可能是一个阻碍。

  • 数组数据不能通过内存映射直接访问。现在numpy具备了这种能力,它对于加载大量数据(更重要的是:在只需要一小部分数据时避免加载大量数据)来说非常有用。

这两个问题都可以通过使用ndarray.tofile()和numpy.fromfile()将原始字节转储到磁盘来解决。然而,这些方法有它们自己的问题

  • 写入的数据没有关于数组形状或dtype的信息。

  • 它无法处理对象数组。

NPY文件格式是这两种方法的演进。其设计主要限于解决pickle和tofile()/fromfile()的问题。它不打算解决更复杂的问题,对于这些问题,HDF5 [2] 等更复杂的格式是更好的解决方案。

用例#

  • 新手Neville刚刚开始学习Python和NumPy。他还没有安装很多包,也没有学习标准库,但他一直在交互式提示符下使用NumPy完成小任务。他得到了一个想要保存的结果。

  • 分析师Annie一直在使用大型嵌套记录数组来表示她的统计数据。她想通过将她的分析代码和数据发送给使用R的同事David Doubter,来说服他Python和NumPy非常棒。她需要数据以交互速度加载。由于David通常不使用Python,需要安装大型包会让他望而却步。

  • 地震学家Simon正在开发新的地震处理工具。他的一种算法需要将大量中间数据写入磁盘。这些数据并不完全符合行业标准SEG-Y模式,但他已经有一个很好的记录数组dtype用于内部使用。

  • 并行计算专家Polly想尽可能简单地在她的多核机器上拆分计算。计算的一部分可以在不同进程之间拆分,无需进程间通信;它们只需要用它们的结果填充大数组的相应部分。让多个子进程内存映射一个公共数组是实现这一目标的好方法。

要求#

该格式必须能够

  • 表示所有NumPy数组,包括嵌套记录数组和对象数组。

  • 以其原生二进制形式表示数据。

  • 包含在单个文件中。

  • 直接支持Fortran连续数组。

  • 存储重建数组所需的所有信息,包括在不同架构机器上的形状和dtype。必须支持小端和大端数组,并且包含小端数字的文件在任何读取该文件的机器上都将生成小端数组。类型必须根据它们的实际大小进行描述。例如,如果一台具有64位C“long int”的机器写入一个包含“long ints”的数组,那么一台具有32位C“long ints”的读取机器将生成一个包含64位整数的数组。

  • 可逆向工程。数据集的寿命通常比创建它们的程序更长。一个有能力的开发人员应该能够在他首选的编程语言中创建解决方案,以便在没有太多文档的情况下读取他获得的NPY文件。

  • 允许数据的内存映射。

  • 可从类文件流对象而不是实际文件中读取。这使得实现易于测试,并使系统更加灵活。NPY文件可以存储在ZIP文件中,并可以轻松地从ZipFile对象中读取。

  • 存储对象数组。由于通用Python对象很复杂,并且只能通过pickle可靠地序列化(如果可以的话),因此包含对象数组的文件可以免除许多其他要求。包含对象数组的文件不必支持内存映射,因为这在技术上是不可能的。我们不能期望在不了解pickle的情况下对pickle格式进行逆向工程。但是,至少应该能够使用与其他数组相同的通用接口读写对象数组。

  • 可使用numpy包本身提供的API进行读写,无需任何其他库。如果必要,numpy内部的实现可以是C语言。

该格式明确不需要

  • 在一个文件中支持多个数组。由于我们要求支持类文件对象,因此可以使用API构建一个支持多个数组的临时格式。然而,解决一般问题和用例超出了此格式和numpy API的范围。

  • 完全处理numpy.ndarray的任意子类。子类将被接受写入,但只写入数组数据。读取文件时将创建一个常规的numpy.ndarray对象。API可用于为特定子类构建格式,但这超出了通用NPY格式的范围。

格式规范:版本1.0#

前6个字节是一个魔术字符串:精确地是“x93NUMPY”。

接下来的1个字节是一个无符号字节:文件格式的主版本号,例如x01。

接下来的1个字节是一个无符号字节:文件格式的次版本号,例如x00。注意:文件格式的版本与numpy包的版本无关。

接下来的2个字节组成一个小端无符号短整型:头部数据HEADER_LEN的长度。

接下来的HEADER_LEN字节构成描述数组格式的头部数据。它是一个ASCII字符串,包含一个Python字典的字面表达式。它以换行符('n')终止,并用空格('x20')填充,使魔术字符串 + 4 + HEADER_LEN的总长度能被16整除,以满足对齐目的。

该字典包含三个键

“descr”dtype.descr

一个可以作为参数传递给numpy.dtype()构造函数以创建数组dtype的对象。

“fortran_order”bool

数组数据是否为Fortran连续。由于Fortran连续数组是非C连续的一种常见形式,我们允许它们直接写入磁盘以提高效率。

“shape”int元组

数组的形状。

为了可重复性和可读性,此字典使用pprint.pformat()进行格式化,因此键按字母顺序排列。

头部之后是数组数据。如果dtype包含Python对象(即dtype.hasobject为True),则数据是数组的Python pickle。否则,数据是数组的连续(C或Fortran,取决于fortran_order)字节。消费者可以通过将形状给出的元素数量(注意shape=()表示有1个元素)乘以dtype.itemsize来计算字节数。

格式规范:版本2.0#

版本1.0格式仅允许数组头部总大小为65535字节。对于具有大量列的结构化数组,这可能会超出。版本2.0格式将头部大小扩展到4 GiB。numpy.save如果数据需要,将自动以2.0格式保存,否则将始终使用更兼容的1.0格式。

因此,头部的第四个元素的描述变为

接下来的4个字节组成一个小端无符号整数:头部数据HEADER_LEN的长度。

约定#

我们建议对此格式的文件使用“.npy”扩展名。这绝不是强制性的;应用程序可能希望使用此文件格式,但使用特定于应用程序的扩展名。然而,在没有明显替代方案的情况下,我们建议使用“.npy”。

为了将多个数组组合到一个文件中,可以使用ZipFile来包含多个“.npy”文件。我们建议这些归档文件使用“.npz”文件扩展名。

替代方案#

作者认为这个系统(或类似的系统)是满足所有要求的最简单的系统。然而,必须始终警惕向世界引入新的二进制格式。

HDF5 [2] 是一种非常灵活的格式,应该能够以某种方式表示NumPy的所有数组。它可能是唯一能够忠实表示NumPy所有数组功能的广泛使用的格式。它在科学界,特别是NumPy社区,得到了大量的采用。对于有或没有NumPy的各种数组存储问题,它都是一个优秀的解决方案。

HDF5是一种复杂的格式,或多或少地实现了文件中的分层文件系统。这一事实使得满足某些要求变得困难。据作者所知,截至本文撰写之时,没有任何应用程序或库能够在不使用规范的libhdf5实现的情况下读写HDF5文件的子集。这个实现是一个庞大的库,并不总是容易构建。将其包含在numpy中是不可行的。

针对HDF5的极其有限的子集可能是可行的。也就是说,其中将只有一个对象:数组。使用连续存储数据,应该能够实现足够多的格式来提供与提议格式相同的元数据。仍然可以满足所有技术要求,如可内存映射性。

通过能够生成其他HDF5软件可读取的文件,我们将获得巨大的好处。此外,通过提供第一个非libhdf5的HDF5实现,我们将能够鼓励在以前因库大小而不可行的应用程序中更多地采用简单的HDF5。这项基础工作可能会鼓励其他语言中类似的极简实现,并进一步扩大社区。

剩下的担忧是关于格式的可逆向工程性。即使是HDF5的简单子集,仅凭一个文件也很难进行逆向工程。然而,考虑到HDF5的显著地位,这可能不是一个实质性的担忧。

总之,我们将继续推进本文件中提出的设计。如果有人编写代码来处理对我们有用的HDF5简单子集,我们可能会考虑修订文件格式。

实现#

版本1.0的实现首次包含在numpy的1.0.5版本中,并仍然可用。版本2.0的实现首次包含在numpy的1.9.0版本中。

具体来说,此目录中的文件format.py实现了此处描述的格式。

参考文献#

[1] https://docs.pythonlang.cn/library/pickle.html

[2] https://support.hdfgroup.org/HDF5/