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“long int”的机器写出一个包含“long int”的数组,那么一台具有 32 位 C“long int”的读取机器将产生一个包含 64 位整数的数组。
可以被逆向工程。数据集通常比创建它们的大程序寿命更长。一个有能力的开发人员应该能够在不查阅太多文档的情况下,使用他喜欢的编程语言创建解决方案来读取他收到的绝大多数 NPY 文件。
允许内存映射数据。
可以从类文件流对象而不是实际文件读取。这使得实现易于测试,并使系统更加灵活。NPY 文件可以存储在 ZIP 文件中,并可以轻松地从 ZipFile 对象读取。
存储对象数组。由于 Python 通用对象很复杂,并且只能通过 pickle(如果可以的话)可靠地序列化,因此对于包含对象数组的文件,许多其他要求都已放宽。包含对象数组的文件不必可 mmap,因为这在技术上是不可能的。我们不能期望在不知道 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”tuple of 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 是一种复杂的格式,它或多或少地实现了文件内分层文件系统。这一事实使得满足某些要求变得困难。据作者所知,在撰写本文时,没有一个应用程序或库能够读取或写入 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 文件实现了此处所述的格式。
参考文献#
版权#
本文档已置于公共领域。