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”int 的 tuple

陣列的形狀。

為了可重複性和可讀性,此字典使用 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 軟體讀取的檔案。此外,透過提供 HDF5 的第一個非 libhdf5 實作,我們將能夠鼓勵在以前由於程式庫大小而不可行的應用程式中更多地採用簡單的 HDF5。基礎工作可能會鼓勵其他語言中出現類似的極簡實作,並進一步擴大社群。

剩餘的疑慮是關於格式的可逆向工程性。即使是 HDF5 的簡單子集,如果僅給定一個檔案本身,也很難進行逆向工程。然而,鑑於 HDF5 的突出地位,這可能不是一個重大的疑慮。

總之,我們將繼續進行本文檔中佈局的設計。如果有人編寫程式碼來處理對我們有用的 HDF5 簡單子集,我們可能會考慮修訂檔案格式。

實作#

版本 1.0 實作首次包含在 numpy 的 1.0.5 版本中,並且仍然可用。版本 2.0 實作首次包含在 numpy 的 1.9.0 版本中。

具體來說,此目錄中的 file format.py 實作了此處描述的格式。

參考文獻#

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

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