NEP 51 — 變更 NumPy 純量值的表示方式#

作者:

Sebastian Berg

狀態:

已接受

類型:

標準追蹤

建立於:

2022-09-13

決議:

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

摘要#

NumPy 具有純量物件(「NumPy 純量值」),代表對應於 NumPy DType 的單一值。這些純量值的表示方式目前與 Python 內建型別的表示方式相符,如下所示:

>>> np.float32(3.0)
3.0

在此 NEP 中,我們提議變更表示方式,以包含 NumPy 純量值類型資訊。將上述範例變更為:

>>> np.float32(3.0)
np.float32(3.0)

我們預期此變更將有助於使用者區分 NumPy 純量值與 Python 內建型別,並釐清其行為。

一旦採用NEP 50,NumPy 純量值與 Python 內建型別之間的區別對於使用者而言將變得更加重要。

這些變更確實會導致與陣列列印相關的較小的不相容性和基礎架構變更。

動機與範疇#

此 NEP 提議變更下列 NumPy 純量值類型的表示方式,以將其與 Python 純量值區分開來:

  • np.bool_

  • np.uint8np.int8 和所有其他整數純量值

  • np.float16np.float32np.float64np.longdouble

  • np.complex64np.complex128np.clongdouble

  • np.str_np.bytes_

  • np.void (結構化 dtype)

此外,其餘 NumPy 純量值的表示方式將調整為列印為 np. 而非 numpy.

  • np.datetime64np.timedelta64

  • np.void (非結構化版本)

NEP 並未提議變更這些純量值的列印方式 – 只會變更其表示方式 (__repr__)。此外,陣列表示方式將不受影響,因為它在必要時已包含 dtype=

此變更背後的主要動機是 Python 數值類型與 NumPy 純量值的行為不同。例如,應謹慎使用精確度較低的數字(例如 uint8float16),使用者應注意他們何時正在使用這些數字。所有 NumPy 整數都可能發生溢位,而 Python 整數則不會。採用 NEP 50 後,這些差異將會加劇,因為精確度較低的 NumPy 純量值將更常被保留。即使是與 Python 的 float 非常相似且繼承自它的 np.float64,其行為也不同,例如在除以零時。

另一個常見的混淆來源是 NumPy 布林值。Python 程式設計師有時會寫入 obj is True,並且當顯示為 True 的物件未能通過測試時會感到驚訝。當值顯示為 np.True_ 時,更容易理解此行為。

我們不僅預期此變更將有助於使用者更好地理解並記住 NumPy 純量值與 Python 純量值之間的差異,而且我們也相信此意識將大大有助於偵錯。

用法與影響#

大多數使用者程式碼不應受到此變更的影響,但是使用者現在將經常看到 NumPy 值顯示為:

np.True_
np.float64(3.0)
np.int64(34)

等等。這也表示文件和 Jupyter Notebook 儲存格中的輸出將經常完整顯示類型資訊。

np.longdoublenp.clongdouble 將使用單引號列印:

np.longdouble('3.0')

以允許往返行程。除了此變更之外,float128 現在將永遠列印為 longdouble,因為舊名稱給人一種錯誤的精確度印象。

向後相容性#

我們預期大多數工作流程不會受到影響,因為只有列印變更。一般而言,我們認為告知使用者他們正在使用的類型比在某些情況下調整列印的需求更重要。

NumPy 測試套件包含諸如 decimal.Decimal(repr(scalar)) 之類的程式碼。此程式碼需要修改為使用 str()

此例外情況是具有文件和尤其是文件測試的下游程式庫。由於許多值的表示方式將會變更,因此在許多情況下,必須更新文件。預計這將在中期需要較大的文件修復。

可能有必要採用 doctest 測試工具,以允許對新表示方式進行近似值檢查。

變更為 arr.tofile()#

arr.tofile() 目前在文字模式下將值儲存為 repr(arr.item())。這並不總是理想的,因為這可能包括轉換為 Python。一個問題是,這會開始將 longdouble 儲存為 np.longdouble('3.1'),這顯然是不希望的。我們預期此方法很少用於物件陣列。對於字串陣列,使用 repr 也會導致儲存 "string"b"string",這似乎很少被需要。

此提案是將預設值(返回)變更為使用 str 而非 repr。如果需要 repr,使用者將必須傳遞 fmt=%r

詳細描述#

此 NEP 提議將 NumPy 純量值的表示方式變更為:

  • np.True_np.False_ 用於布林值(它們的單例實例)

  • np.scalar(<值>),即 np.float64(3.0) 用於所有數值 dtype。

  • np.longdoublenp.clongdouble 的值將以引號括起來:np.longdouble('3.0')。這可確保它始終可以正確往返行程,並且符合 decimal.Decimal 的行為方式。對於這兩者,將不會使用基於大小的名稱(例如 float128),因為實際大小取決於平台,因此具有誤導性。

  • np.str_("string")np.bytes_(b"byte_string") 用於字串 dtype。

  • np.void((3, 5), dtype=[('a', '<i8'), ('b', 'u1')])(類似於陣列)用於結構化類型。這將是重新建立純量值的有效語法。

與陣列不同,純量值表示方式應正確往返行程,因此 longdouble 值將被引號括起來,而其他值永遠不會被截斷。

在某些位置(即遮罩陣列、void 和記錄純量值),我們希望列印不包含類型的表示方式。例如:

np.void(('3.0',), dtype=[('a', 'f16')])  # longdouble

應使用引號列印 3.0(以確保往返行程),但不重複完整的 np.longdouble('3.0'),因為 dtype 包含 longdouble 資訊。為了允許這樣做,將引入新的半公開 np.core.array_print.get_formatter(),以擴展目前的功能(請參閱「實作」)。

對遮罩陣列和記錄的影響#

NumPy 的某些其他部分將間接受到變更。fill_value 遮罩陣列將調整為僅包含完整的純量值資訊,例如 fill_value=np.float64(1e20),當陣列的 dtype 不符時。對於 longdouble(具有相符的 dtype),它將列印為 fill_value='3.1',包括引號,這(原則上但實際上可能不會)確保往返行程。應注意,對於字串,dtype 通常在字串長度上不符。因此,字串通常將列印為 np.str_("N/A")

np.record 純量值將與 np.void 對齊,並與其列印相同(除了名稱本身)。例如,如下所示:np.record((3, 5), dtype=[('a', '<i8'), ('b', 'u1')])

關於 longdoubleclongdouble 的詳細資訊#

對於 longdoubleclongdouble 值,例如:

np.sqrt(np.longdouble(2.))

除非以字串形式引號括起來,否則可能無法往返行程(因為轉換為 Python float 會損失精確度)。此 NEP 提議使用單引號,類似於 Python 的 decimal,後者列印為 Decimal('3.0')

longdouble 可以具有不同的精確度和儲存大小,範圍從 8 到 16 個位元組不等。但是,即使 float128 是正確的,因為數字以 128 位元儲存,但它通常不具有 128 位元精確度。(clongdouble 是相同的,但儲存大小是兩倍。)

因此,此 NEP 包含變更 longdouble 名稱的提案,使其始終列印為 longdouble,而不是 float128float96。它不包含棄用 np.float128 別名。但是,此類棄用可能會獨立於 NEP 發生。

整數純量值類型名稱和實例表示方式#

一個細節是,由於 NumPy 純量值類型基於 C 類型,NumPy 有時會區分它們,例如在大多數 64 位元系統上(非 Windows):

>>> np.longlong
numpy.longlong
>>> np.longlong(3)
np.int64(3)

此提案將導致類型使用 longlong 名稱,同時純量值使用 int64 形式。做出此選擇是因為 int64 通常對使用者而言是更有用的資訊,但是類型名稱本身必須精確。

實作#

注意

此部分尚未初始 PR 中實作。將需要類似的變更來修復列印中的某些情況,並允許完全正確地列印,例如包含 longdouble 的結構化純量值。預計未來也需要類似的解決方案,以允許自訂 DType 正確列印。

新的表示方式主要可以在純量值類型上實作,而測試套件中需要最大的變更。

void 純量值和遮罩 fill_value 的建議變更使得必須公開不包含類型的純量值表示方式。

我們提議引入半公開 API:

np.core.arrayprint.get_formatter(*,
        data=None, dtype=None, fmt=None, options=None)

以取代目前的內部 _get_formatting_func。與舊函式相比,這將允許兩件事:

  • data 可以是 None(如果傳遞 dtype),允許不傳遞稍後將列印/格式化的多個值。

  • fmt= 將允許在未來將格式字串傳遞給 DType 特定的元素格式器。目前,get_formatter() 將接受 reprstr(單例而非字串)以格式化不包含類型資訊的元素('3.1' 而非 np.longdouble('3.1'))。實作確保格式化符合,除了類型資訊之外。

    空的格式字串將以與 str() 相同的方式列印(當傳遞資料時,可能會額外填補)。

get_formatter() 預計將在未來查詢使用者 DType 的方法,以允許自訂所有 DType 的格式化。

公開 get_formatter 允許將其用於 np.record 和遮罩陣列。目前,格式器本身似乎是半公開的;使用單一進入點有望為格式化 NumPy 值提供清晰的 API。

純量值表示方式變更的絕大部分先前已由 Ganesh Kathiresan 在 [2] 中完成。

替代方案#

可以考慮不同的表示方式:替代方案包括將 np. 拼寫為 numpy.,或從數值純量值中刪除 np. 部分。我們認為使用 np. 已足夠清晰、簡潔,並且允許複製貼上表示方式。僅使用 float64(3.0) 而不使用 np. 字首更簡潔,但是可能存在 NumPy 依賴關係不完全清晰且名稱可能與其他程式庫衝突的情況。

對於布林值,替代方案是使用 np.bool_(True)bool_(True)。但是,NumPy 布林純量值是單例,並且建議的格式化更簡潔。先前在 [1] 中也討論了布林值的替代方案。

對於字串純量值,混淆通常較不明顯。延遲變更這些可能是合理的。

非有限值#

此提案不允許複製貼上 naninf 值。它們可以用 np.float64('nan')np.float64(np.nan) 表示。這更簡潔,Python 也使用 naninf,而不是允許透過將其顯示為 float('nan') 來複製貼上。可以說,這在 NumPy 中會是一個較小的補充,其中將始終列印。

get_formatter() 的替代方案#

當傳遞 fmt= 時,特別是對於主要用途(在此 NEP 中)格式化為 reprstr。也可以使用 ufunc 或直接格式化函式,而不是將其包裝到 `get_formatter() 中,後者依賴於實例化 DType 的格式器類別。

此 NEP 並未排除建立 ufunc 或建立特殊路徑。但是,NumPy 陣列格式化通常會查看所有要格式化的值,以便新增填補以進行對齊或提供統一的指數輸出。在這種情況下,會傳遞 data= 並在準備中使用。這種格式化形式(與想要 data=None 的純量值情況不同)不幸地與 UFunc 從根本上不相容。

使用單例 strrepr 可確保未來的格式化字串(如 f"{arr:r}")不會因使用 "r""s" 而受到任何限制。

討論#

參考文獻與註腳#