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

需求#

该格式必须能够

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

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

  • 包含在单个文件中。

  • 直接支持 Fortran 连续数组。

  • 存储重建数组所需的所有信息,包括在不同体系结构的机器上进行形状和 dtype。必须支持小端和大端数组,并且具有小端数字的文件将在读取文件的任何机器上生成小端数组。这些类型必须根据它们实际的大小进行描述。例如,如果具有 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”。

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

下一个字节是一个无符号字节:文件格式的次要版本号,例如 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=() 表示存在 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 是一种复杂的格式,它或多或少地实现了一个文件中的层次文件系统。这一事实使得满足某些需求变得困难。据作者所知,截至撰写本文时,还没有应用程序或库能够读取或写入甚至 HDF5 文件的子集,而不会使用规范的 libhdf5 实现。此实现是一个大型库,并不总是容易构建。将其包含在 numpy 中是不可行的。

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

能够生成可以被其他 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/