NEP 41 — 邁向新型資料型別系統的第一步#

標題:

邁向新型資料型別系統的第一步

作者:

Sebastian Berg

作者:

Stéfan van der Walt

作者:

Matti Picus

狀態:

已接受

類型:

標準追蹤

建立時間:

2020-02-03

決議:

https://mail.python.org/pipermail/numpy-discussion/2020-April/080573.htmlhttps://mail.python.org/pipermail/numpy-discussion/2020-March/080495.html

注意

此 NEP 是系列中的第二篇

  • NEP 40 解釋了 NumPy dtype 實作的缺點。

  • NEP 41(本文檔)概述了我們提議的替代方案。

  • NEP 42 描述了新設計中與資料型別相關的 API。

  • NEP 43 描述了新設計中用於通用函數的 API。

摘要#

資料型別 在 NumPy 中描述如何解釋陣列中的每個元素。NumPy 提供了 intfloatcomplex 數值型別,以及字串、datetime 和結構化資料型別功能。然而,不斷成長的 Python 社群需要更多樣化的資料型別。範例包括附加單位資訊的資料型別(例如公尺)或類別資料型別(固定的可能值集合)。然而,目前的 NumPy 資料型別 API 過於受限,無法建立這些型別。

此 NEP 是實現此類成長的第一步;它將為新型資料型別帶來更簡單的開發路徑。從長遠來看,新型資料型別系統也將支援直接從 Python 而非 C 建立資料型別。重構資料型別 API 將提高可維護性,並促進使用者定義的外部資料型別以及 NumPy 內部現有資料型別的新功能的開發。

動機與範疇#

另請參閱

使用者影響章節包含新型資料型別在長期內將實現的範例。因此,它可能有助於不按順序閱讀這些章節。

動機#

目前 API 的主要問題之一是參數化資料型別的典型函數定義,例如加法和乘法(另請參閱 NEP 40),這需要額外步驟來確定輸出型別。例如,當將兩個長度為 4 的字串相加時,結果是長度為 8 的字串,這與輸入不同。同樣,嵌入物理單位的資料型別必須計算新的單位資訊:將距離除以時間會得到速度。相關的困難是 目前的轉換規則 – 不同資料型別之間的轉換 – 無法描述為 NumPy 外部實作的此類參數化資料型別的轉換。

這種支援參數化資料型別的額外功能增加了 NumPy 本身內部的複雜性,而且對於外部使用者定義的資料型別不可用。一般而言,不同資料型別的考量並未良好地封裝。內部 C 結構的暴露加劇了這種負擔,限制了新增欄位(例如支援新的排序方法 [new_sort])。

目前,許多因素限制了新型使用者定義資料型別的建立

  • 為參數化使用者定義 dtype 建立轉換規則是不可能的,或者非常複雜以至於從未嘗試過。

  • 型別提升,例如決定將浮點值和整數值相加應傳回浮點值的運算,對於數值資料型別非常有價值,但對於使用者定義的資料型別,尤其是參數化資料型別,其範圍有限。

  • 大部分邏輯(例如提升)都寫在單個函數中,而不是作為資料型別本身的方法分割。

  • 在目前的設計中,資料型別不能有不通用於其他資料型別的方法。例如,單位資料型別不能有 .to_si() 方法,以輕鬆找到以 SI 單位表示相同值的資料型別。

解決這些問題的巨大需求驅使科學社群在多個專案中建立解決方案,將物理單位實作為類似陣列的類別,而不是資料型別,這將更好地通用於多個類似陣列的類別(Dask、pandas 等)。Pandas 已經透過其擴展陣列 [pandas_extension_arrays] 朝著相同的方向邁進,毫無疑問,如果此類新功能可以在 NumPy、Pandas 和其他專案之間通用,社群將獲得最佳服務。

範疇#

提議的資料型別系統重構是一項龐大的工程,因此建議分為多個階段,大致如下

  • 階段 I:重組和擴展資料型別基礎架構(此 NEP 41)

  • 階段 II:逐步定義或修改 API(主要在 NEP 42/43 中詳細說明)

  • 階段 III:NumPy 和科學 Python 生態系統功能的成長。

有關各個階段的更詳細說明,請參閱下文實作章節中的「完整重構方法計畫」。此 NEP 提議繼續進行必要的新 dtype 子類別的建立(階段 I),並開始著手實作目前的功能。在此 NEP 的範圍內,所有開發都將完全是私有 API 或使用未來必須變更的初步帶底線的名稱。大部分內部和公共 API 選擇是第二階段的一部分,將在後續的 NEP 42 和 43 中更詳細地討論。此 NEP 的初始實作對使用者幾乎沒有影響,但為逐步解決完整的重構提供了必要的基礎工作。

此 NEP 及其後續實作,暗示了 NumPy 中資料型別定義方式的大規模重構,預計會造成小的相容性問題(請參閱向下相容性章節)。然而,預計不會出現需要大規模程式碼調整的過渡,也不在範圍內。

具體而言,此 NEP 做出以下設計選擇,這些選擇在詳細描述章節中進行了更詳細的討論

  1. 每個資料型別都將是 np.dtype 子類別的實例,其中大部分資料型別特定邏輯都實作為類別上的特殊方法。在 C-API 中,這些對應於特定的插槽。簡而言之,對於 f = np.dtype("f8")isinstance(f, np.dtype) 將保持為 true,但 type(f) 將是 np.dtype 的子類別,而不是僅僅 np.dtype 本身。目前儲存為實例指標(作為 PyArray_Descr->f)的 PyArray_ArrFuncs 應改為儲存在類別上,就像在 Python 中通常所做的那樣。未來,這些可能對應於 Python 端的雙底線方法。儲存資訊(例如 itemsize 和 byteorder)在不同的 dtype 實例之間可能有所不同(例如「S3」與「S8」),並且將保留為實例的一部分。這表示從長遠來看,目前對 dtype 方法的低階存取將被移除(請參閱 NEP 40 中的 PyArray_ArrFuncs)。

  2. 目前的 NumPy 純量將不會變更,它們不會是資料型別的實例。對於新型資料型別也是如此,純量不會是 dtype 的實例(雖然 isinstance(scalar, dtype) 在適當的時候可能會傳回 True)。

詳細的技術決策將在 NEP 42 中繼續說明。

此外,公共 API 的設計方式將使其未來可擴展

  1. 提供給使用者的所有新型 C-API 函數都將盡可能隱藏實作細節。公共 API 應是內部 NumPy 資料型別所用 C-API 的相同但受限的版本。

資料型別系統的目標可能是與 NumPy 陣列協同運作,例如透過提供步幅迴圈,但應避免與陣列物件(通常是 np.ndarray 實例)直接互動。相反,設計原則是陣列物件是資料型別的消費者。雖然僅僅是一個指導原則,但這可能會允許將資料型別系統甚至 NumPy 資料型別分割為 NumPy 依賴的獨立專案。

階段 II 中對資料型別系統的變更必須包括 UFunc 機制的大規模重構,這將在 NEP 43 中進一步定義

  1. 為了為新型使用者定義資料型別啟用所有所需的功能,UFunc 機制將被變更,以取代目前的調度和型別解析系統。舊系統應在一段時間內大部分作為舊版版本受到支援。

此外,作為一般的設計原則,新增使用者定義的資料型別將不會變更程式的行為。例如,除非 ab 知道 c 存在,否則 common_dtype(a, b) 不得為 c

使用者影響#

目前的生態系統中使用 NumPy 的使用者定義資料型別非常少,其中最突出的是:rationalquaternion。這些表示相當簡單的資料型別,不受目前限制的強烈影響。然而,我們已經確定了對以下資料型別的需求,例如

  • bfloat16,用於深度學習

  • 類別型別

  • 物理單位(例如公尺)

  • 用於追蹤/自動微分的資料型別

  • 高精度、固定精度數學

  • 特殊整數型別,例如 int2、int24

  • 新的、更好的 datetime 表示法

  • 擴展例如整數 dtype 以具有哨兵 NA 值

  • 幾何物件 [pygeos]

其中一些問題已部分解決;例如,單位功能在 astropy.unitsunytpint 中以 numpy.ndarray 子類別的形式提供。然而,這些資料型別中的大多數現在根本無法合理定義。在 NumPy 中擁有此類資料型別的優勢在於,它們應與其他陣列或類似陣列的套件(例如 Pandas、xarray [xarray_dtype_issue]Dask)無縫整合。

實作此 NEP 的長期使用者影響將是透過擁有此類新型資料型別來促進整個生態系統的成長,以及在 NumPy 中整合此類資料型別的實作,以實現更好的互通性。

範例#

以下範例代表我們希望啟用的未來使用者定義資料型別。這些資料型別不屬於 NEP 的一部分,並且選擇(例如轉換規則的選擇)是我們希望啟用的可能性,並不代表建議。

簡單數值型別#

主要用於考慮記憶體的場合,較低精度的數值型別(例如 bfloat16)在其他計算框架中很常見。對於這些型別,np.common_typenp.can_cast 等定義是一些最重要的介面。一旦它們支援 np.common_type,(在大多數情況下)就可以找到正確的 ufunc 迴圈來呼叫,因為大多數 ufunc – 例如 add – 實際上只需要 np.result_type

>>> np.add(arr1, arr2).dtype == np.result_type(arr1, arr2)

~numpy.result_type 在很大程度上與 ~numpy.common_type 相同。

固定、高精度數學#

允許任意精度或更高精度的數學運算在模擬中非常重要。例如,mpmath 定義了一個精度

>>> import mpmath as mp
>>> print(mp.dps)  # the current (default) precision
15

NumPy 應能夠從 mpmath.mpf 浮點物件清單建構原生、記憶體效率高的陣列

>>> arr_15_dps = np.array(mp.arange(3))  # (mp.arange returns a list)
>>> print(arr_15_dps)  # Must find the correct precision from the objects:
array(['0.0', '1.0', '2.0'], dtype=mpf[dps=15])

我們也應該能夠在為陣列建立資料型別時指定所需的精度。在這裡,我們使用 np.dtype[mp.mpf] 來尋找 DType 類別(此表示法不屬於此 NEP 的一部分),然後使用所需的參數實例化該類別。這也可以寫成 MpfDType 類別

>>> arr_100_dps = np.array([1, 2, 3], dtype=np.dtype[mp.mpf](dps=100))
>>> print(arr_15_dps + arr_100_dps)
array(['0.0', '2.0', '4.0'], dtype=mpf[dps=100])

mpf 資料型別可以決定運算的結果應為兩者中較高的精度,因此使用 100 的精度。此外,我們應該能夠定義轉換,例如在

>>> np.can_cast(arr_15_dps.dtype, arr_100_dps.dtype, casting="safe")
True
>>> np.can_cast(arr_100_dps.dtype, arr_15_dps.dtype, casting="safe")
False  # loses precision
>>> np.can_cast(arr_100_dps.dtype, arr_100_dps.dtype, casting="same_kind")
True

從 float 轉換可能始終至少是 same_kind 轉換,但一般而言,這並不安全

>>> np.can_cast(np.float64, np.dtype[mp.mpf](dps=4), casting="safe")
False

因為 float64 的精度高於 dps=4mpf 資料型別。

或者,我們可以說

>>> np.common_type(np.dtype[mp.mpf](dps=5), np.dtype[mp.mpf](dps=10))
np.dtype[mp.mpf](dps=10)

甚至可能

>>> np.common_type(np.dtype[mp.mpf](dps=5), np.float64)
np.dtype[mp.mpf](dps=16)  # equivalent precision to float64 (I believe)

因為 np.float64 可以安全地轉換為 np.dtype[mp.mpf](dps=16)

類別#

類別的有趣之處在於,它們可以具有固定的、預定義的值,也可以是動態的,並且能夠在必要時修改類別。固定類別(預先定義)是最直接的類別定義。類別是困難的,因為實作它們有很多策略,這表示 NumPy 應僅為使用者定義的類別型別提供支架。例如

>>> cat = Categorical(["eggs", "spam", "toast"])
>>> breakfast = array(["eggs", "spam", "eggs", "toast"], dtype=cat)

可以非常有效率地儲存陣列,因為它知道只有 3 個類別。由於在這個意義上的類別幾乎不了解儲存在其中的資料,因此很少有運算有意義,儘管相等性確實有意義

>>> breakfast2 = array(["eggs", "eggs", "eggs", "eggs"], dtype=cat)
>>> breakfast == breakfast2
array[True, False, True, False])

類別資料型別可以像字典一樣運作:沒有兩個項目名稱可以相等(在 dtype 建立時檢查),因此可以非常有效率地執行上述相等運算。如果值定義了順序,則可以以相同的方式對類別標籤(內部整數)進行排序,以允許有效率的排序和比較。

是否定義從具有較少值的類別到具有嚴格更多定義值的類別的轉換,這是類別資料型別需要決定的事情。兩種選項都應可用。

資料型別上的單位#

定義單位的方法有很多種,具體取決於內部機制的組織方式,一種方法是為每個現有的數值型別提供單個單位資料型別。這將寫為 Unit[float64],單位本身是 DType 實例的一部分,Unit[float64]("m") 是附加公尺的 float64

>>> from astropy import units
>>> meters = np.array([1, 2, 3], dtype=np.float64) * units.m  # meters
>>> print(meters)
array([1.0, 2.0, 3.0], dtype=Unit[float64]("m"))

請注意,單位有點棘手。是否可爭議

>>> np.array([1.0, 2.0, 3.0], dtype=Unit[float64]("m"))

應為有效語法(將沒有單位的浮點純量強制轉換為公尺)。一旦建立陣列,數學運算將在沒有任何問題的情況下運作

>>> meters / (2 * unit.seconds)
array([0.5, 1.0, 1.5], dtype=Unit[float64]("m/s"))

從一個單位到另一個單位的轉換無效,但在相同維度的不同尺度之間可能有效(儘管這可能是「不安全的」)

>>> meters.astype(Unit[float64]("s"))
TypeError: Cannot cast meters to seconds.
>>> meters.astype(Unit[float64]("km"))
>>> # Convert to centimeter-gram-second (cgs) units:
>>> meters.astype(meters.dtype.to_cgs())

上述表示法有點笨拙。可以使用函數來代替在單位之間轉換。可能有方法使這些更方便,但這些必須留待以後討論

>>> units.convert(meters, "km")
>>> units.to_cgs(meters)

有一些未解決的問題。例如,陣列物件上是否可以存在其他方法來簡化某些概念,以及這些方法將如何從資料型別滲透到 ndarray

與其他純量的互動很可能透過以下方式定義

>>> np.common_type(np.float64, Unit)
Unit[np.float64](dimensionless)

Ufunc 輸出資料型別確定可能比簡單數值 dtype 更複雜,因為沒有「通用」輸出型別

>>> np.multiply(meters, seconds).dtype != np.result_type(meters, seconds)

實際上,在沒有完成運算的上下文的情況下,np.result_type(meters, seconds) 必須出錯。此範例強調了特定的 ufunc 迴圈(具有已知、特定 DType 作為輸入的迴圈)如何在實際計算開始之前做出某些決策。

實作#

完整重構方法計畫#

為了解決 NumPy 中的這些問題並啟用新型資料型別,需要多個開發階段

  • 階段 I:重組和擴展資料型別基礎架構(此 NEP)

    • 像正常的 Python 類別一樣組織資料型別 [PR 15508]_

  • 階段 II:逐步定義或修改 API

    • 透過 DType 上的方法和屬性逐步定義所有必要的功能 (NEP 42)

      • 類別層次結構和 DType 類別本身的屬性,包括以下最核心點未涵蓋的方法。

      • 將支援使用 arr.astype() 進行 dtype 轉換以及轉換相關運算(例如 np.common_type)的功能。

      • 項目存取和儲存的實作,以及使用 np.array() 建立陣列時確定形狀和 dtype 的方式

      • 建立公共 C-API 以定義新的 DType。

    • 重組通用函數的工作方式 (NEP 43),以允許為使用者定義的資料型別(例如單位)擴展 ~numpy.ufunc(例如 np.add

      • 重構低階 C 函數的組織方式,使其具有可擴展性和足夠的靈活性,以應對複雜的 DType(例如單位)。

      • 實作使用者定義的這些低階 C 函數的註冊和有效率的查詢。

      • 定義在需要轉換時將如何使用提升來實作行為。例如,np.float64(3) + np.int32(3)int32 提升為 float64

  • 階段 III:NumPy 和科學 Python 生態系統功能的成長

    • 清理被認為有錯誤或不受歡迎的舊版行為。

    • 提供從 Python 定義新型資料型別的路徑。

    • 協助社群建立單位或類別等型別

    • 允許在函數(例如 np.equalnp.add)中使用字串。

    • 移除 NumPy 內的舊版程式碼路徑,以提高長期可維護性

本文檔作為階段 I 的基礎,並為整個專案提供了願景和動機。階段 I 不會引入任何新的使用者介面功能,而是關注目前資料型別系統的必要概念清理。它提供了更「Python 化」的資料型別 Python 型別物件,具有清晰的類別層次結構。

第二階段是逐步建立定義完整資料型別和重組 NumPy 資料型別系統所需的所有 API。因此,此階段將主要關注定義初始的、初步的穩定公共 API。

大規模重構的一些好處可能僅在完全棄用目前的舊版實作(即更大的程式碼移除)後才會顯現出來。然而,這些步驟對於改進核心 NumPy API 的許多部分是必要的,並且預計將使實作更容易理解。

下圖在高階層次說明了提議的設計,並大致劃分了整體設計的組件。請注意,此 NEP 僅涉及階段 I(陰影區域),其餘部分包含階段 II,並且設計選擇尚待討論,但是,它突顯了 DType 資料型別類別是核心的、必要的概念

_images/nep-0041-mindmap.svg

向下相容性#

雖然實作階段 I 和 II 的實際向下相容性影響尚未完全清楚,但我們預期並接受以下變更

  • Python API:

    • type(np.dtype("f8")) 將是 np.dtype 的子類別,而現在 type(np.dtype("f8")) is np.dtype。程式碼應使用 isinstance 檢查,並且在極少數情況下可能必須調整為使用它。

  • C-API:

    • 在舊版本的 NumPy 中,PyArray_DescrCheck 是一個巨集,它使用 type(dtype) is np.dtype。當針對舊版本的 NumPy 進行編譯時,可能必須以對應的 PyObject_IsInstance 呼叫取代該巨集。(如果這是一個問題,我們可以回溯修正該巨集)

    • UFunc 機制變更將破壞目前實作的有限部分。預計在一段時間內仍將支援取代例如預設的 TypeResolver,儘管不再支援最佳化的遮罩內部迴圈迭代(甚至在 NumPy 也沒有使用)。

    • 目前在 dtype 上定義的所有函數(例如 PyArray_Descr->f->nonzero)將以不同的方式定義和存取。這表示從長遠來看,低階存取程式碼將必須變更為使用新的 API。預計此類變更在極少數專案中是必要的。

  • dtype 實作者 (C-API):

    • 目前提供給某些函數(例如轉換函數)的陣列將不再提供。例如,PyArray_Descr->f->nonzeroPyArray_Descr->f->copyswapn,可能會改為接收僅具有某些欄位(主要是 dtype)的虛擬陣列物件,這些欄位是有效的。至少在某些程式碼路徑中,已經使用了類似的機制。

    • scalarkind 插槽和純量轉換的註冊將被移除/忽略,而不會被取代。它目前允許基於部分值的轉換。PyArray_ScalarKind 函數將繼續適用於內建型別,但不會在內部使用並且將被棄用。

    • 目前,使用者 dtype 定義為 np.dtype 的實例。建立工作原理是使用者提供原型實例。NumPy 將需要在註冊期間至少修改型別。這對 rationalquaternion 都沒有影響,並且在註冊後結構的變更似乎不太可能。

由於存在相當大的 API 介面與資料型別相關,因此很可能發生進一步的變更或將某些函數限制為目前現有的資料型別。例如,應以採用 DType 類別的函數取代使用型別編號作為輸入的函數。儘管是公開的,但此 C-API 的大部分似乎很少被下游專案使用,甚至可能從未使用過。

詳細描述#

本節詳細說明了此 NEP 涵蓋的設計決策。小節對應於範疇章節中提出的設計選擇清單。

作為 Python 類別的資料型別 (1)#

目前的 NumPy 資料型別不是完整的 Python 類別。相反,它們是單個 np.dtype 類別的(原型)實例。變更此設定表示任何特殊處理,例如針對 datetime 的特殊處理,都可以移至 Datetime DType 類別,從而擺脫單體的通用程式碼(例如目前的 PyArray_AdjustFlexibleDType)。

關於 API 的這項變更,主要的影響是特殊方法將從 dtype 實例移至新的 DType 類別的方法。這是 Python 中使用的典型設計模式。以更符合 Python 風格的方式組織這些方法和資訊,為未來改進和擴展 API 奠定了堅實的基礎。目前的 API 由於其公開方式而無法擴展。例如,這表示目前儲存在每個資料類型上的 PyArray_ArrFuncs 中的方法(請參閱 NEP 40)將在未來以不同的方式定義,並在長期內被棄用。

最顯著可見的副作用是 type(np.dtype(np.float64)) 將不再是 np.dtype。相反地,它將是 np.dtype 的子類別,這表示 isinstance(np.dtype(np.float64), np.dtype) 仍將為真。這也將新增使用 isinstance(dtype, np.dtype[float64]) 的能力,從而不再需要使用 dtype.kinddtype.chardtype.type 進行此檢查。

隨著將 DType 作為完整 Python 類別的設計決策,子類別化的問題隨之而來。然而,對於容器資料類型來說,繼承似乎有問題,而且最好避免這種複雜性(至少在初期)。此外,子類別對於與 GPU 後端(CuPy)的互操作性可能更有趣,例如儲存與 GPU 相關的其他方法,而不是作為定義新資料類型的機制。類別層次結構確實提供了價值,並且可以透過允許建立抽象資料類型來實現。抽象資料類型的一個例子是與 np.floating 等效的資料類型,表示任何浮點數。這些可以達到與 Python 的抽象基底類別相同的目的。

此 NEP 選擇完整或部分複製純量層次結構。主要原因是將 DType 和純量的實作解耦。為了將 DType 新增到 NumPy,理論上不需要修改純量,也不需要讓純量知道 NumPy。另請注意,目前在 pandas 中實作的類別 DType 沒有純量對應,因此依賴純量來實作行為不太直接。雖然 DType 和純量描述相同的概念/類型(例如 int64),但將 NumPy 必要的資訊和功能拆分到 DType 類別中似乎是可行的。

dtype 實例提供參數和儲存選項#

從電腦科學的角度來看,類型定義了值空間(其例項可以採用的所有可能值)及其行為。正如本 NEP 中提出的,DType 類別定義了值空間和行為。dtype 實例可以被視為值的一部分,因此典型的 Python instance 對應於 dtype + element(其中 element 是儲存在陣列中的資料)。另一種觀點是在 dtype 實例上直接定義值空間和行為。以下圖中呈現了這兩個選項,並將其與類似的 Python 實作模式進行了比較

_images/nep-0041-type-sketch-no-fonts.svg

差異在於如何處理參數(例如字串長度或日期時間單位(msns 等))和儲存選項(例如位元組順序)。當實作 Python(純量)type 參數時,例如日期時間單位,將儲存在實例中。這是 NEP 42 嘗試模仿的設計,但是,參數現在是 dtype 實例的一部分,這表示儲存在實例中的部分資料由所有陣列元素共享。如先前所述,這表示 Python instance 對應於儲存在 NumPy 陣列中的 dtype + element

Python 中更進階的方法是使用類別工廠和抽象基底類別 (ABC)。這允許將參數移動到動態建立的 type 中,並且行為實作可能特定於這些參數。另一種方法可能會使用此模型,並直接在 dtype 實例上實作行為。

我們認為這裡提出的版本更易於使用和理解。Python 類別工廠並不常用,NumPy 也沒有使用針對 dtype 參數或位元組順序的專門程式碼。讓實作此類專業化更容易似乎不是優先事項。此選擇的一個結果是,如果某些 DType 沒有參數或儲存變體,則它們可能只有單例實例。但是,由於允許附加元資料,因此所有 NumPy dtype 都需要動態建立的實例。

純量不應是資料類型的實例 (2)#

對於諸如 float64 之類的簡單資料類型(另請參閱下文),np.dtype("float64") 的實例可以是純量,這似乎很吸引人。由於純量(而不是資料類型)目前定義了有用的類型層次結構,因此這個想法可能更具吸引力。

然而,由於許多原因,我們已明確決定反對這樣做。首先,本文所述的新資料類型將是 DType 類別的實例。雖然有可能將這些實例本身設為類別,但這會增加使用者需要理解的額外複雜性。這也意味著純量必須具有儲存資訊(例如位元組順序),這通常是不必要的,並且目前未使用。其次,雖然諸如 float64 之類的簡單 NumPy 純量可能是此類實例,但應該可以為不強制 NumPy 作為依賴項的 Python 物件建立資料類型。但是,不依賴 NumPy 的 Python 物件不能是 NumPy DType 的實例。第三,對於純量和資料類型有用的方法和屬性之間存在不匹配。例如,to_float() 對於純量有意義,但對於資料類型則沒有意義,而 newbyteorder 在純量上沒有用處(或具有不同的含義)。

總體而言,似乎將純量設為 DType 的實例,並不是降低複雜性(即透過合併兩個不同的類型層次結構),而是會增加設計和實作的複雜性。

未來可能的路徑可能是簡化目前的 NumPy 純量,使其成為更簡單的物件,這些物件主要從資料類型衍生其行為。

用於建立新資料類型的 C-API (3)#

目前使用者可用於建立新資料類型的 C-API 在範圍上受到限制,並且需要使用「私有」結構。這表示 API 不可擴展:不丟失二進制相容性的情況下,無法將新成員新增到結構中。這已經限制了將新的排序方法納入 NumPy [new_sort]

因此,新版本應取代目前用於定義新資料類型的 PyArray_ArrFuncs 結構。在棄用期間,將支援目前存在且使用這些插槽定義的資料類型。

最有可能的解決方案是向使用者隱藏實作,從而使其在未來可擴展,即仿照 Python 的穩定 API [PEP-384] 來建模 API

static struct PyArrayMethodDef slots[] = {
    {NPY_dt_method, method_implementation},
    ...,
    {0, NULL}
}

typedef struct{
  PyTypeObject *typeobj;  /* type of python scalar */
  ...;
  PyType_Slot *slots;
} PyArrayDTypeMeta_Spec;

PyObject* PyArray_InitDTypeMetaFromSpec(
        PyArray_DTypeMeta *user_dtype, PyArrayDTypeMeta_Spec *dtype_spec);

C 端插槽的設計應反映 Python 端方法,例如 dtype.__dtype_method__,儘管向 Python 公開是實作的後續步驟,目的是降低初始實作的複雜性。

UFunc 機制的 C-API 變更 (4)#

擬議的 UFunc 機制變更將是 NEP 43 的一部分。但是,以下變更將是必要的(有關目前實作及其問題的詳細描述,請參閱 NEP 40

  • 目前的 UFunc 類型解析必須進行調整,以便更好地控制使用者定義的 dtype,並解決目前的不一致性。

  • UFunc 中使用的內迴圈必須擴展為包含傳回值。此外,必須改進錯誤報告,並啟用傳遞 dtype 特定資訊。這需要修改內迴圈函數簽名,並新增在內迴圈使用前後呼叫的新掛鉤。

通用函數變更的一個重要目標是允許重複使用現有的迴圈。對於新的單位資料類型來說,在處理與單位相關的計算後,應該很容易回退到現有的數學函數。

討論#

有關先前會議和討論的列表,請參閱 NEP 40

關於此特定 NEP 的其他討論已在郵件列表和提取請求中進行

參考文獻#

致謝#

為 NumPy 建立新資料類型的努力已在許多不同的背景和設定中討論多年,以至於無法列出所有參與者。我們要特別感謝 Stephan Hoyer、Nathaniel Smith 和 Eric Wieser 就資料類型設計進行了多次深入討論。我們非常感謝社群在審查和修訂此 NEP 中的投入,並要特別感謝 Ross Barnowski 和 Ralf Gommers。