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 Newbie 刚刚开始学习 Python 和 NumPy。他还没有安装很多包,也没有学习标准库,但他一直在交互式提示符下使用 NumPy 来完成一些小任务。他得到了想要保存的结果。

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

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

  • Polly Parallel 想尽可能简单地在她多核机器上分割计算。计算的部分可以在不同的进程之间分割,而进程之间没有任何通信;它们只需要用它们的结果填充大型数组的适当部分即可。让几个子进程内存映射一个公共数组是实现此目标的好方法。

需求#

该格式必须能够

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

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

  • 包含在一个文件中。

  • 直接支持 Fortran 连续数组。

  • 存储重建数组所需的所有信息,包括在不同架构的机器上使用的形状和 dtype。必须支持小端和大端数组,并且包含小端数字的文件将在读取文件的任何机器上产生小端数组。必须根据其实际大小来描述类型。例如,如果具有 64 位 C“长整数”的机器写出包含“长整数”的数组,则具有 32 位 C“长整数”的读取机器将产生包含 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 连续的。由于 Fortran 连续数组是非 C 连续性的常见形式,因此我们允许将其直接写入磁盘以提高效率。

“shape”整数元组

数组的形状。

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

标题之后是数组数据。如果 dtype 包含 Python 对象(即 dtype.hasobject 为 True),则数据是数组的 Python pickle。否则,数据是数组的连续字节(取决于 fortran_order,为 C 顺序或 Fortran 顺序)。使用者可以通过将形状给出的元素数量(注意 shape=() 表示只有一个元素)乘以 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 是一种复杂的格式,或多或少地实现了一个文件中的分层文件系统。这个事实使得满足某些需求变得困难。据作者所知,截至撰写本文时,还没有任何应用程序或库能够读取或写入 HDF5 文件的任何子集,而不用使用规范的 libhdf5 实现。此实现是一个大型库,并不总是容易构建。将其包含在 numpy 中是不可行的。

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

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

剩下的问题是关于格式的反向工程能力。即使是 HDF5 的简单子集,仅凭文件本身也很难反向工程。但是,鉴于 HDF5 的突出地位,这可能不是一个重大问题。

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

实现#

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

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

参考文献#

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

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