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

作者

罗伯特·科恩<罗伯特.克恩@gmail ​ com >

地位

最终的

创建

2007 年 12 月 20 日

抽象的

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

理由#

人们经常需要一个轻量级、无所不在的系统来将 NumPy 数组保存到磁盘。 Python 通常有 pickle [1] 用于将大多数 Python 对象保存到磁盘。这通常可以很好地与 NumPy 数组一起用于多种用途,但它有一些缺点:

  • 转储或加载 pickle 文件需要复制内存中的数据。对于大型阵列,这可能是一个令人震惊的问题。

  • 数组数据不能通过内存映射直接访问。现在 numpy 具有这种功能,事实证明它对于加载大量数据非常有用(或者更重要的是:当您只需要一小部分数据时,避免加载大量数据)。

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

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

  • 它无法处理对象数组。

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

用例

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

  • Annie Analyst 一直使用大型嵌套记录数组来表示她的统计数据。她想通过向使用 R 的同事 David Doubter 发送她的分析代码和数据,让他相信 Python 和 NumPy 非常棒。她需要以交互速度加载数据。由于 David 通常不使用 Python,因此需要安装大型软件包会让他厌烦。

  • 西蒙地震学家正在开发新的地震处理工具。他的算法之一需要将大量中间数据写入磁盘。该数据并不真正适合行业标准的 SEG-Y 模式,但他已经有一个很好的记录数组数据类型可以在内部使用它。

  • Polly Parallel 希望尽可能简单地拆分她的多核计算机上的计算。部分计算可以分配给不同的进程,进程之间无需任何通信;他们只需要用结果填充一个大数组的适当部分。让多个子进程内存映射一个公共数组是实现此目的的好方法。

要求

该格式必须能够:

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

  • 以本机二进制形式表示数据。

  • 包含在单个文件中。

  • 直接支持 Fortran 连续数组。

  • 在不同架构的机器上存储重建数组所需的所有信息,包括形状和数据类型。必须支持小端和大端数组,并且具有小端数字的文件将在读取该文件的任何机器上生成小端数组。类型必须根据其实际尺寸进行描述。例如,如果具有 64 位 C“long int”的机器写出具有“long int”的数组,则具有 32 位 C“long int”的读取机器将生成具有 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”布尔值

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

int 的“shape”元组

阵列的形状。

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

标题后面是数组数据。如果 dtype 包含 Python 对象(即 dtype.hasobject 为 True),则数据是数组的 Python pickle。否则,数据是数组的连续(C 或 Fortran,取决于 fortran_order)字节。消费者可以通过将 shape 给出的元素数量(注意 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 软件读取的文件,我们将获得巨大的好处。此外,通过提供 HDF5 的第一个非 libhdf5 实现,我们将能够鼓励在以前由于库的大小而无法实现的应用程序中更多地采用简单的 HDF5。基础工作可能会鼓励其他语言中类似的极其简单的实现,并进一步扩大社区。

剩下的问题是该格式的逆向工程能力。即使是 HDF5 的简单子集,仅给出一个文件本身也很难进行逆向工程。然而,鉴于 HDF5 的突出地位,这可能不是一个重大问题。

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

执行

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

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

参考

[1] https://docs.python.org/library/pickle.html

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