NEP 30 — NumPy 陣列的鴨子型別 - 實作#

作者:

Peter Andreas Entschev <pentschev@nvidia.com>

作者:

Stephan Hoyer <shoyer@google.com>

狀態:

已取代

取代者:

NEP 56 — NumPy 主命名空間中對 Array API 標準的支援

類型:

標準追蹤

建立日期:

2019-07-31

更新日期:

2019-07-31

決議:

https://mail.python.org/archives/list/numpy-discussion@python.org/message/Z6AA5CL47NHBNEPTFWYOTSUVSRDGHYPN/

摘要#

我們提議 __duckarray__ 協定,遵循 NEP 22 中描述的高階概述,允許下游函式庫傳回其定義型別的陣列,而不是 np.asarray,後者會將這些 array_like 物件強制轉換為 NumPy 陣列。

詳細描述#

NumPy 的 API,包含陣列定義,已在無數其他專案中實作和模仿。根據定義,許多這些陣列在操作方式上與 NumPy 標準非常相似。__array_function__ 的引入允許直接透過 NumPy 的 API 調度由這些專案中的幾個實作的函式。這引入了一個新的需求,即傳回類似 NumPy 的陣列本身,而不是強制轉換為純 NumPy 陣列。

為了上述目的,NEP 22 將鴨子型別的概念引入 NumPy 陣列。NEP 中建議的解決方案允許函式庫避免在必要時將類似 NumPy 的陣列強制轉換為純 NumPy 陣列,同時仍然允許不希望實作該協定的類似 NumPy 陣列函式庫透過 np.asarray 將陣列強制轉換為純 NumPy 陣列。

使用指南#

使用 np.duckarray 的程式碼旨在支援其他「遵循 NumPy API」的類似 ndarray 的物件。目前這是一個定義不明確的概念 – 每個已知的函式庫僅部分實作 NumPy API,並且許多函式庫至少在某些細微方面有意偏離。這不容易補救,因此對於 np.duckarray 的使用者,我們建議以下策略:檢查在您使用 np.duckarray 後的程式碼所使用的 NumPy 功能是否存在於 Dask、CuPy 和 Sparse 中。如果是,則合理預期任何鴨子陣列都可以在此處運作。如果不是,我們建議您在您的文件字串中指出接受哪些種類的鴨子陣列,或者它們需要具備哪些屬性。

為了舉例說明鴨子陣列的用法,假設有人想要取得類似陣列的物件 arrmean()。使用 NumPy 達成此目的,可以寫成 np.asarray(arr).mean() 以達成預期的結果。如果 arr 不是 NumPy 陣列,這將建立一個實際的 NumPy 陣列以便呼叫 .mean()。但是,如果陣列是符合 NumPy API 的物件(全部或部分符合),例如 CuPy、Sparse 或 Dask 陣列,則該複製將是不必要的。另一方面,如果有人要使用新的 __duckarray__ 協定:np.duckarray(arr).mean(),並且 arr 是符合 NumPy API 的物件,它將只會被傳回,而不是被強制轉換為純 NumPy 陣列,從而避免不必要的複製和潛在的效能損失。

實作方式#

實作想法相當簡單,需要在 NumPy 中引入一個新的函式 duckarray,以及在類似 NumPy 的陣列類別中引入一個新的方法 __duckarray__。新的 __duckarray__ 方法應傳回下游類似陣列的物件本身,例如 self 物件,而 __array__ 方法則引發 TypeError。或者,__array__ 方法可以建立一個實際的 NumPy 陣列並傳回它。

新的 NumPy duckarray 函式可以如下實作

def duckarray(array_like):
    if hasattr(array_like, '__duckarray__'):
        return array_like.__duckarray__()
    return np.asarray(array_like)

實作類似 NumPy 陣列的專案範例#

現在考慮一個函式庫,它實作了一個名為 NumPyLikeArray 的 NumPy 相容陣列類別,這個類別應實作上述方法,完整的實作方式如下所示

class NumPyLikeArray:
    def __duckarray__(self):
        return self

    def __array__(self):
        raise TypeError("NumPyLikeArray can not be converted to a NumPy "
                         "array. You may want to use np.duckarray() instead.")

上面的實作範例是最簡單的情況,但總體思路是函式庫將實作一個 __duckarray__ 方法,該方法傳回原始物件,以及一個 __array__ 方法,該方法可以建立並傳回適當的 NumPy 陣列,或引發 ``TypeError`` 以防止在 NumPy 陣列中意外用作物件(如果在不實作 __array__ 的任意物件上呼叫 np.asarray,它將建立一個 NumPy 陣列純量)。

對於尚未實作 __array__ 但想要使用鴨子陣列型別的現有函式庫,建議他們同時引入 __array__ 和 ``__duckarray__`` 方法。

用法#

以下範例展示了如何使用 __duckarray__ 協定來編寫基於 concatenatestack 函式,以及其產生的結果。此處選擇此範例不僅是為了示範 duckarray 函式的用法,也是為了示範它對 NumPy API 的依賴性,透過檢查陣列的 shape 屬性來示範。請注意,此範例僅是 NumPy 實際實作在第一個軸上運作的 stack 的簡化版本,並且假設 Dask 已實作 __duckarray__ 方法。

def duckarray_stack(arrays):
    arrays = [np.duckarray(arr) for arr in arrays]

    shapes = {arr.shape for arr in arrays}
    if len(shapes) != 1:
        raise ValueError('all input arrays must have the same shape')

    expanded_arrays = [arr[np.newaxis, ...] for arr in arrays]
    return np.concatenate(expanded_arrays, axis=0)

dask_arr = dask.array.arange(10)
np_arr = np.arange(10)
np_like = list(range(10))

duckarray_stack((dask_arr, dask_arr))   # Returns dask.array
duckarray_stack((dask_arr, np_arr))     # Returns dask.array
duckarray_stack((dask_arr, np_like))    # Returns dask.array

相反地,僅使用 np.asarray(在本 NEP 撰寫時,這是函式庫開發人員通常採用的方法,以確保陣列類似 NumPy)會產生不同的結果

def asarray_stack(arrays):
    arrays = [np.asanyarray(arr) for arr in arrays]

    # The remaining implementation is the same as that of
    # ``duckarray_stack`` above

asarray_stack((dask_arr, dask_arr))     # Returns np.ndarray
asarray_stack((dask_arr, np_arr))       # Returns np.ndarray
asarray_stack((dask_arr, np_like))      # Returns np.ndarray

向後相容性#

此提案在 NumPy 內部不會引起任何向後相容性問題,因為它僅引入了一個新函式。但是,選擇引入 __duckarray__ 協定的下游函式庫可能會選擇移除透過 np.arraynp.asarray 函式將陣列強制轉換回 NumPy 陣列的能力,從而防止此類陣列強制轉換回純 NumPy 陣列的意外影響(正如某些函式庫已經做的那樣,例如 CuPy 和 Sparse),但仍然讓未實作該協定的函式庫可以選擇使用 np.duckarray 來將 array_like 物件提升為純 NumPy 陣列。

先前的提案和討論#

此處提出的鴨子型別協定在 NEP 22 中以高階方式描述。

此外,關於協定和相關提案的更長討論發生在 numpy/numpy #13831