NEP 22 — NumPy 陣列的鴨子型別 – 高階概述#

作者:

Stephan Hoyer <shoyer@google.com>, Nathaniel J. Smith <njs@pobox.com>

狀態:

最終

類型:

資訊性

建立於:

2018-03-22

決議:

https://mail.python.org/pipermail/numpy-discussion/2018-September/078752.html

摘要#

我們概述了 NumPy 如何處理「鴨子陣列」的高階願景。這是一份資訊類 NEP;它並未針對任何特定實作規定完整細節。簡而言之,我們建議開發一些新的協定,以定義具有與 NumPy 相符的高階 API 的多維陣列實作。

詳細描述#

傳統上,NumPy 的 ndarray 物件提供了兩件事:用於對同質型別、任意維度、陣列結構化資料進行表達式運算的高階 API,以及基於步進式記憶體內儲存的 API 的具體實作。此 API 功能強大、相當通用,並廣泛用於整個科學 Python 生態系。另一方面,具體實作適用於廣泛的用途,但存在限制:隨著資料集成長且 NumPy 被用於各種新環境中,步進式記憶體內儲存策略越來越不適用,使用者發現他們需要稀疏陣列、延遲評估陣列(如 dask 中)、壓縮陣列(如 blosc 中)、儲存在 GPU 記憶體中的陣列、儲存在 Arrow 等替代格式中的陣列等等 – 然而使用者仍然希望使用熟悉的 NumPy API 來處理這些陣列,並以最小(理想情況下為零)的移植開銷重複使用現有程式碼。作為工作速記,我們將這些稱為「鴨子陣列」,類比於 Python 的「鴨子型別」:「鴨子陣列」是一個 Python 物件,它「像鴨子一樣叫」,因為它具有相同或相似的 Python API,但不共享 C 語言層級的實作。

此 NEP 並未提出對 NumPy 或其他專案的任何具體變更;相反地,它概述了我們希望如何擴展 NumPy 以支援實作和依賴其高階 API 的健全專案生態系。

術語#

「鴨子陣列」作為目前的佔位符號運作良好,但它相當於行話,可能會讓新使用者感到困惑,因此我們可能想要為實際的 API 函式選擇其他名稱。不幸的是,「array-like」已經被用於「可以強制轉換為陣列的任何事物」(包括例如列表物件)的概念,而「anyarray」已經被用於「共享 ndarray 的實作,但具有不同語意的事物」的概念,這與鴨子陣列相反(例如,np.matrix 是一個「anyarray」,但不是「鴨子陣列」)。這是一個經典的腳踏車棚問題,因此目前我們僅使用「鴨子陣列」。不過,一些可能的選項包括:arrayish、pseudoarray、nominalarray、ersatzarray、arraymimic、…

一般方法#

在高階層級上,鴨子陣列支援需要處理 NumPy 提供的每個 API 函式,並弄清楚如何擴展它以與鴨子陣列物件一起運作。在某些情況下,這很容易(例如,ndarray 本身的方法/屬性);在其他情況下,這更困難。以下是我們目前發現的一些有用的原則

原則 1:專注於「完整」鴨子陣列,但不要排除「部分」鴨子陣列#

我們可以區分兩個類別

  • 「完整」鴨子陣列,它們渴望完全實作 np.ndarray 的 Python 層級 API,並且幾乎可以在任何 np.ndarray 運作的地方運作

  • 「部分」鴨子陣列,它們有意僅實作 np.ndarray API 的子集。

完整的鴨子陣列,嗯,有點無聊。它們具有與 ndarray 完全相同的語意,差異僅限於關於資料實際儲存方式的底層決策。那些對使 numpy 更具可擴展性感到興奮的人,也不足為奇地對變更或擴展 numpy 的語意感到興奮。因此,已經有很多關於如何最好地支援部分鴨子陣列的討論。我們自己也曾犯過這個錯誤。

不過,在這一點上,我們認為最佳的總體策略是將我們的努力主要集中在支援完整鴨子陣列上,並且僅在我們需要確保我們不會因為沒有理由而意外排除它們的情況下才擔心部分鴨子陣列。

為什麼要專注於完整鴨子陣列?幾個原因

首先,有很多非常明確的用例。完整鴨子陣列介面的潛在消費者包括幾乎每個使用 numpy 的套件(scipy、sklearn、astropy、…),尤其是提供處理多種陣列類型的陣列包裝類別的套件,例如 xarray 和 dask.array。完整鴨子陣列介面的潛在實作者包括:分散式陣列、稀疏陣列、遮罩陣列、具有單位的陣列(除非它們切換為使用 dtype)、標記陣列等等。明確的用例會帶來良好且相關的 API。

其次,安娜·卡列尼娜原則在這裡適用:完整的鴨子陣列都一樣,但每個部分的鴨子陣列都有自己的部分性

  • xarray.DataArray 大部分是鴨子陣列,但具有不相容的廣播語意。

  • xarray.Dataset 在一個物件中包裝多個陣列;它仍然實作一些陣列介面,例如 __array_ufunc__,但當然不是全部。

  • pandas.Series 具有與 numpy 行為類似的方法,但具有獨特的空值跳過行為。

  • scipy 的 LinearOperator 僅支援矩陣乘法,而沒有其他功能

  • 用於存取陣列儲存的 h5py 和類似程式庫具有支援類似 numpy 的切片和轉換為完整陣列的物件,但不支援計算。

  • 某些類別可能與 ndarray 類似,但不支援完整的索引語意。

諸如此類。

儘管我們盡了最大努力,但我們尚未找到任何明確、獨特的方法可以將 ndarray API 切割成相關類型的層次結構來捕捉這些區別;事實上,不太可能有人甚至理解所有區別。而這很重要,因為我們有大量的 API 需要新增鴨子陣列支援(在 numpy 以及所有依賴 numpy 的專案中!)。依照定義,這些已經適用於 ndarray,因此希望讓它們適用於完整鴨子陣列應該不會那麼困難,因為依照定義,完整鴨子陣列的行為就像 ndarray 一樣。必須瀏覽每個函式並識別它需要的 ndarray API 的確切子集,然後弄清楚哪些部分陣列類型可以/應該支援它,這將非常麻煩。一旦我們讓完整鴨子陣列正常運作,我們就可以稍後再回去並根據需要進一步完善所需的 API。專注於完整鴨子陣列可讓我們立即開始取得進展。

未來,識別鴨子陣列的特定用例並標準化僅針對這些用例的更窄介面可能會很有用。例如,擁有一個標準的「陣列載入器」介面可能很有意義,讓 h5py、netcdf、pydap、zarr 等檔案存取程式庫都實作,以便輕鬆在這些程式庫之間切換。但那是我們可以隨著時間推移做的事情,而且不一定需要 NumPy 開發人員參與。如需可能的外觀範例,請參閱 dask.array.from_array 的文件。

原則 2:利用鴨子型別#

ndarray 具有非常大的 API 表面積

In [1]: len(set(dir(np.ndarray)) - set(dir(object)))
Out[1]: 138

而這是一個巨大的低估,因為 NumPy 和其他程式庫中也有許多獨立函式,它們目前使用 NumPy C API,因此僅適用於 ndarray 物件。在型別理論中,型別是由您可以對物件執行的運算定義的;因此,ndarray 的實際型別不僅包括其方法和屬性,還包括所有這些函式。為了讓鴨子陣列取得成功,它們需要實作很大一部分 ndarray API – 但不是全部。(例如,dask.array.Array 沒有提供與 ndarray.ptp 方法等效的方法,大概是因為從來沒有人注意到或關心它的缺失。但這似乎並沒有阻止人們使用 dask。)

這表示實際上,我們不能期望預先定義整個鴨子陣列 API,或者任何人都能夠一次實作所有內容;這將是一個漸進的過程。這也表示即使是所謂的「完整」鴨子陣列介面在邊界處也有些模糊定義;np.ndarray API 的某些部分是鴨子陣列不需要實作的,但我們不完全確定這些部分是什麼。

最終,定義什麼符合或不符合鴨子陣列的資格並不是 NumPy 開發人員的責任。如果我們希望 scikit-learn 函式在 dask 陣列上運作(例如),那麼這將需要這兩個專案之間的協商來發現不相容性,並且當發現不相容性時,將由他們協商誰應該變更以及如何變更。NumPy 專案可以提供技術工具和一般建議來協助解決這些分歧,但我們不能強迫某個群體或另一個群體對任何給定的錯誤承擔責任。

因此,即使我們專注於「完整」鴨子陣列,我們也不嘗試定義規範性的「陣列 ABC」– 也許有一天這會很有用,但現在,還不是時候。作為一個方便的副作用,缺乏規範性定義為部分鴨子陣列留下了實驗空間。

但是,我們在下面為鴨子陣列實作者和消費者提供了一些更詳細的建議。

原則 3:專注於協定#

從歷史上看,numpy 在透過定義協定與第三方物件互通方面取得了許多成功,例如 __array__(要求任意物件將其自身轉換為陣列)、__array_interface__(Python 緩衝區協定的前身)和 __array_ufunc__(允許第三方物件支援 ufuncs,例如 np.exp)。

NEP 16 採取了不同的方法:我們需要一個鴨子陣列等效於 asarray,並且它建議透過定義一個版本的 asarray 來做到這一點,該版本將允許透過實作新的 AbstractArray ABC 的物件。如上所述,我們現在認為嘗試定義 ABC 出於其他原因是一個壞主意。但是當在郵件列表中討論此 NEP 時,我們意識到即使就其自身優點而言,這個想法也不是那麼好。更好的方法是定義一個方法,可以在任意物件上呼叫該方法,要求它將自身轉換為鴨子陣列,然後定義一個版本的 asarray,該版本呼叫此方法。

這嚴格來說更強大:如果物件已經是鴨子陣列,它可以簡單地 return self。它允許更正確的語意:NEP 16 假設 asarray(obj, dtype=X)asarray(obj).astype(X) 相同,但事實並非如此。它支援更多用例:如果 h5py 支援稀疏陣列,它可能想要提供一個物件,該物件本身不是稀疏陣列,但可以自動轉換為稀疏陣列。請參閱 NEP <XX,待撰寫> 以取得完整詳細資訊。

協定方法也更符合核心 Python 慣例:例如,請參閱用於強制物件轉換為迭代器的 __iter__ 方法,或用於安全整數強制轉換的 __index__ 協定。最後,專注於協定為部分鴨子陣列敞開了大門,部分鴨子陣列可以挑選並選擇它們想要參與的協定子集,每個協定都有明確定義的語意。

結論:協定是一個非常棒的主意 – 讓我們做更多這些。

原則 4:盡可能重複使用現有方法#

嘗試定義清理後的 ndarray 方法版本,並使用更簡潔的介面以便於實作,這很誘人。例如,__array_reshape__ 可以捨棄 reshape 接受的一些奇怪的引數,而 __array_basic_getitem__ 可以捨棄 NumPy 高階索引的所有 奇怪的邊緣情況

但如上所述,我們並不真正知道鴨子型別 ndarray 需要哪些 API。我們最終將得到一個非常長的新特殊方法列表。相反,現有方法(如 reshape__getitem__)的優點是已經被使用鴨子陣列的程式庫廣泛使用/練習,而且實際上,任何嚴重的鴨子陣列類型無論如何都必須實作它們。

原則 5:讓做正確的事情變得容易#

讓鴨子陣列良好運作將是一項社群努力。文件有所幫助,但僅此而已。我們希望讓實作能做正確事情的鴨子陣列變得容易。

NumPy 可以提供幫助的一種方式是提供混合類別,用於一次實作大量相關功能。NDArrayOperatorsMixin 是一個很好的例子:它允許透過 __array_ufunc__ 方法隱式實作算術運算子。它並不完整,我們將需要更多類似的輔助程式(例如,用於歸約)。

(我們最初認為這些混合類別的重要性可能是提供陣列 ABC 的論點,因為那是現代 Python 中執行混合類別的標準方式。但在圍繞 NEP 16 的討論中,我們意識到部分鴨子陣列在某些情況下也希望利用這些混合類別,因此即使我們確實有陣列 ABC,那麼混合類別仍然需要某種獨立的存在。所以別介意這個論點。)

暫定的鴨子陣列指南#

作為一般規則,使用鴨子陣列的程式庫應堅持最低可能的要求,而實作鴨子陣列的程式庫應提供盡可能完整的 API。這將確保最大的相容性。例如,使用者應優先依賴 .transpose() 而不是 .swapaxes()(可以根據 transpose 實作),但鴨子陣列作者理想情況下應同時實作兩者。

如果您嘗試實作鴨子陣列,則應努力實作所有內容。您肯定需要 .shape.ndim.dtype,但您的 dtype 屬性實際上也應該是一個 numpy.dtype 物件,奇怪的進階索引邊緣情況理想情況下應該有效等等。只有與 NumPy 特定 np.ndarray 實作相關的細節(例如,stridesdataview)才明確超出範圍。

未來計畫的(非常)粗略草圖#

到目前為止討論的提案 – __array_ufunc__ 和某種 asarray 協定 – 顯然是必要的,但不足以完全支援鴨子型別。我們預期需要額外的協定來支援(至少)這些功能

  • 串聯鴨子陣列,這將在其他陣列組合方法(如 stack/vstack/hstack)內部使用。串聯的實作將需要在陣列引數列表中協商。我們預期使用類似 __array_ufunc____array_concatenate__ 協定,而不是多重分派。

  • 目前不是 ufuncs 的類似 Ufunc 的函式。許多 NumPy 函式(如 median、percentile、sort、where 和 clip)可以寫成廣義 ufuncs,但目前不是。這些函式應該寫成 ufuncs,或者我們應該考慮新增另一個通用包裝機制,該機制的工作方式與 ufuncs 類似,但對實作方式的保證較少。

  • 與鴨子陣列一起使用的隨機數產生,例如,np.random.randn()。例如,我們可能想要新增新的 API,例如 random_like(),用於產生具有相符形狀類型的陣列 – 儘管我們需要查看這些函式如何使用的實際範例,以弄清楚什麼會有幫助。

  • 其他雜項函式,例如 np.einsumnp.zeros_likenp.broadcast_to,它們不屬於上述任何類別。

  • 檢查鴨子陣列的可變性,這表示它們支援使用 __setitem__ 進行賦值以及 ufuncs 的 out 引數。許多其他方面良好的鴨子陣列不易變更(例如,因為它們使用某些種類的稀疏或壓縮儲存,或位於唯讀共享記憶體中),並且事實證明,經常使用的程式碼(如 np.mean 的預設實作)需要檢查這一點(以決定是否可以重複使用暫時陣列)。

我們在此處有意不描述如何確切新增對這些類型鴨子陣列的支援。這些將成為未來 NEP 的主題。