NEP 51 — 變更 NumPy 純量值的表示方式#
- 作者:
Sebastian Berg
- 狀態:
已接受
- 類型:
標準追蹤
- 建立於:
2022-09-13
- 決議:
摘要#
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.uint8
、np.int8
和所有其他整數純量值np.float16
、np.float32
、np.float64
、np.longdouble
np.complex64
、np.complex128
、np.clongdouble
np.str_
、np.bytes_
np.void
(結構化 dtype)
此外,其餘 NumPy 純量值的表示方式將調整為列印為 np.
而非 numpy.
np.datetime64
和np.timedelta64
np.void
(非結構化版本)
NEP 並未提議變更這些純量值的列印方式 – 只會變更其表示方式 (__repr__
)。此外,陣列表示方式將不受影響,因為它在必要時已包含 dtype=
。
此變更背後的主要動機是 Python 數值類型與 NumPy 純量值的行為不同。例如,應謹慎使用精確度較低的數字(例如 uint8
或 float16
),使用者應注意他們何時正在使用這些數字。所有 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.longdouble
和 np.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.longdouble
和np.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')])
關於 longdouble
和 clongdouble
的詳細資訊#
對於 longdouble
和 clongdouble
值,例如:
np.sqrt(np.longdouble(2.))
除非以字串形式引號括起來,否則可能無法往返行程(因為轉換為 Python float 會損失精確度)。此 NEP 提議使用單引號,類似於 Python 的 decimal,後者列印為 Decimal('3.0')
longdouble
可以具有不同的精確度和儲存大小,範圍從 8 到 16 個位元組不等。但是,即使 float128
是正確的,因為數字以 128 位元儲存,但它通常不具有 128 位元精確度。(clongdouble
是相同的,但儲存大小是兩倍。)
因此,此 NEP 包含變更 longdouble
名稱的提案,使其始終列印為 longdouble
,而不是 float128
或 float96
。它不包含棄用 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()
將接受repr
或str
(單例而非字串)以格式化不包含類型資訊的元素('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] 中也討論了布林值的替代方案。
對於字串純量值,混淆通常較不明顯。延遲變更這些可能是合理的。
非有限值#
此提案不允許複製貼上 nan
和 inf
值。它們可以用 np.float64('nan')
或 np.float64(np.nan)
表示。這更簡潔,Python 也使用 nan
和 inf
,而不是允許透過將其顯示為 float('nan')
來複製貼上。可以說,這在 NumPy 中會是一個較小的補充,其中將始終列印。
新 get_formatter()
的替代方案#
當傳遞 fmt=
時,特別是對於主要用途(在此 NEP 中)格式化為 repr
或 str
。也可以使用 ufunc 或直接格式化函式,而不是將其包裝到 `get_formatter()
中,後者依賴於實例化 DType 的格式器類別。
此 NEP 並未排除建立 ufunc 或建立特殊路徑。但是,NumPy 陣列格式化通常會查看所有要格式化的值,以便新增填補以進行對齊或提供統一的指數輸出。在這種情況下,會傳遞 data=
並在準備中使用。這種格式化形式(與想要 data=None
的純量值情況不同)不幸地與 UFunc 從根本上不相容。
使用單例 str
和 repr
可確保未來的格式化字串(如 f"{arr:r}"
)不會因使用 "r"
或 "s"
而受到任何限制。
討論#
關於此變更的討論發生在郵件清單執行緒中:https://mail.python.org/archives/list/numpy-discussion@python.org/thread/7GLGFHTZHJ6KQPOLMVY64OM6IC6KVMYI/
先前有一個問題 [1] 和 PR [2] 僅變更 NumPy 布林值的表示方式。PR 後來更新為變更所有(或至少大多數)NumPy 純量值的表示方式。
參考文獻與註腳#
著作權#
本文檔已置於公共領域。