NEP 50 — Python 純量升級規則#

作者:

Sebastian Berg

狀態:

最終

類型:

標準追蹤

建立於:

2021-05-25

摘要#

自 NumPy 1.7 以來,升級規則使用所謂的「安全轉換」,這依賴於檢查涉及的值。這有助於識別使用者的許多邊緣情況,但實作起來很複雜,也使行為難以預測。

有兩種令人困惑的結果

  1. 基於值的升級意味著值(例如 Python 整數的值)可以決定輸出類型,如 np.result_type 所發現的那樣

    np.result_type(np.int8, 1) == np.int8
    np.result_type(np.int8, 255) == np.int16
    

    這種邏輯的產生是因為 1 可以用 uint8int8 表示,而 255 無法用 int8 表示,但只能用 uint8int16 表示。

    這也適用於使用 0-D 陣列(所謂的「純量陣列」)時

    int64_0d_array = np.array(1, dtype=np.int64)
    np.result_type(np.int8, int64_0d_array) == np.int8
    

    其中 int64_0d_array 具有 int64 dtype 的事實對結果 dtype 沒有影響。dtype=np.int64 在此範例中被有效地忽略,因為只有其值重要。

  2. 對於 Python intfloatcomplex,值會如先前所示進行檢查。但令人驚訝的是,當 NumPy 物件是 0-D 陣列或 NumPy 純量時,不會 進行檢查

    np.result_type(np.array(1, dtype=np.uint8), 1) == np.int64
    np.result_type(np.int8(1), 1) == np.int64
    

    原因是當所有物件都是純量或 0-D 陣列時,基於值的升級會被停用。因此,NumPy 會傳回與 np.array(1) 相同的類型,通常是 int64(這取決於系統)。

請注意,這些範例也適用於乘法、加法、比較等運算,以及它們對應的函數,例如 np.multiply

此 NEP 提議根據以下兩個指導原則重構行為

  1. 值絕不能影響結果類型。

  2. NumPy 純量和 0-D 陣列的行為應與其 N-D 對應項一致。

我們提議移除所有基於值的邏輯,並為 Python 純量新增特殊處理,以保留一些方便的行為。Python 純量將被視為「弱」類型。當 NumPy 陣列/純量與 Python 純量結合時,它將被轉換為 NumPy dtype,因此

np.array([1, 2, 3], dtype=np.uint8) + 1  # returns a uint8 array
np.array([1, 2, 3], dtype=np.float32) + 2.  # returns a float32 array

將不會依賴 Python 值本身。

提議的變更也適用於 np.can_cast(100, np.int8),但是,我們預期函數(升級)中的行為實際上將比轉換變更本身更重要。

注意

截至 NumPy 1.24.x 系列,NumPy 具有初步和有限的支援來測試此提案。

進一步需要設定以下環境變數

export NPY_PROMOTION_STATE=weak

有效值為 weakweak_and_warnlegacy。請注意,weak_and_warn 實作了此 NEP 中提議的可選警告,並且預計會非常 吵雜。我們建議開始使用 weak 選項,並主要使用 weak_and_warn 來了解特定觀察到的行為變更。

存在以下額外 API

  • np._set_promotion_state()np._get_promotion_state(),它們等效於環境變數。(非執行緒/上下文安全。)

  • with np._no_nep50_warning(): 允許在使用 weak_and_warn 升級時抑制警告。(執行緒和上下文安全。)

此時,整數冪的溢位警告遺失。此外,np.can_castweak_and_warn 模式下無法給出警告。其關於 Python 純量輸入的行為可能仍在變動中(這應該影響非常少的使用者)。

新提議的升級規則的架構#

變更後,NumPy 中的升級將遵循以下架構。升級始終沿著綠線發生:在其種類內從左到右,並且僅在必要時才升級到更高的種類。結果種類始終是輸入中最大的種類。請注意,float32 的精度低於 int32uint32,因此在示意圖中稍微向左排序。這是因為 float32 無法精確表示所有 int32 值。但是,出於實際原因,NumPy 允許將 int64 升級到 float64,實際上將它們視為具有相同的精度。

Python 純量插入到每個「種類」的最左側,並且 Python 整數不區分有符號和無符號。因此,NumPy 升級使用以下已排序的種類類別

  • 布林值

  • 整數: 有符號或無符號整數

  • 非精確: 浮點數和複數浮點數

當使用較高種類類別(布林值 < 整數 < 非精確)升級 Python 純量與較低種類類別時,我們使用最小/預設精度:即 float64complex128int64(在某些系統上使用 int32,例如 Windows)。

_images/nep-0050-promotion-no-fonts.svg

請參閱下一節中的範例,以闡明提議的行為。在下表中可以找到更多範例,以及與目前行為的比較。

新行為範例#

為了更容易理解上述文字和圖表,我們提供了一些新行為的範例。在下面,Python 整數對結果類型沒有影響

np.uint8(1) + 1 == np.uint8(2)
np.int16(2) + 2 == np.int16(4)

在以下範例中,Python floatcomplex 是「非精確的」,但 NumPy 值是整數,因此我們至少使用 float64/complex128

np.uint16(3) + 3.0 == np.float64(6.0)
np.int16(4) + 4j == np.complex128(4+4j)

但這不會發生在 floatcomplex 的升級中,其中 float32complex64 具有相同的精度

np.float32(5) + 5j == np.complex64(5+5j)

請注意,示意圖省略了 bool。它設定在「整數」下方,因此以下成立

np.bool_(True) + 1 == np.int64(2)
True + np.uint8(2) == np.uint8(3)

請注意,雖然此 NEP 使用簡單運算符作為範例,但描述的規則通常適用於所有 NumPy 運算。

比較新舊行為的表格#

下表列出了相關的變更和未變更的行為。請參閱 舊實作 以取得導致「舊結果」的規則的詳細說明,以及以下章節中詳細說明新規則的內容。向後相容性章節討論了這些變更可能如何影響使用者。

請注意 0-D 陣列(如 array(2))與非 0-D 陣列(如 array([2]))之間的重要區別。

已變更行為的表格#

表達式

舊結果

新結果

uint8(1) + 2

int64(3)

uint8(3) [T1]

array([1], uint8) + int64(1)

array([1], uint8) + array(1, int64)

array([2], uint8)

array([2], int64) [T2]

array([1.], float32) + float64(1.)

array([1.], float32) + array(1., float64)

array([2.], float32)

array([2.], float64)

array([1], uint8) + 1

array([2], uint8)

未變更

array([1], uint8) + 200

array([201], np.uint8)

未變更

array([100], uint8) + 200

array([ 44], uint8)

未變更 [T3]

array([1], uint8) + 300

array([301], uint16)

例外 [T4]

uint8(1) + 300

int64(301)

例外 [T5]

uint8(100) + 200

int64(300)

uint8(44) RuntimeWarning [T6]

float32(1) + 3e100

float64(3e100)

float32(Inf) RuntimeWarning [T7]

array([1.0], float32) + 1e-14 == 1.0 [T8]

array([True])

未變更

array(1.0, float32) + 1e-14 == 1.0 [T8]

False

True

array([1.], float32) + 3

array([4.], float32)

未變更

array([1.], float32) + int64(3)

array([4.], float32)

array([4.], float64) [T9]

(3j + array(3, complex64)).dtype

complex128

complex64 [T10]

(float32(1) + 1j)).dtype

complex128

complex64 [T11]

(int32(1) + 5j).dtype

complex128

未變更 [T12]

[T1]

新行為尊重 uint8 純量的 dtype。

[T2]

目前的 NumPy 在與陣列結合時,會忽略 0-D 陣列或 NumPy 純量的精度。

[T3]

目前的 NumPy 在與陣列結合時,會忽略 0-D 陣列或 NumPy 純量的精度。

[T4]

舊行為使用 uint16,因為 300 不適合 uint8,新行為出於相同原因引發錯誤。

[T5]

300 無法轉換為 uint8

[T6]

可能是最危險的變更之一。保留類型會導致溢位。NumPy 純量會發出指示溢位的 RuntimeWarning

[T7]

np.float32(3e100) 溢位到無限大並發出警告。

[T8] (1,2)

1 + 1e-14 在 float32 中完成時會損失精度,但在 float64 中則不會。舊行為根據陣列的維度以不同方式將純量引數轉換為 float32 或 float64;使用新行為,計算始終以陣列精度(在此情況下為 float32)完成。

[T9]

NumPy 將 float32int64 升級到 float64。舊行為在此處忽略了 int64

[T10]

新行為在 array(3, complex64)array([3], complex64) 之間保持一致:結果的 dtype 是陣列引數的 dtype。

[T11]

新行為使用與陣列引數 float32 相容的精度的複數 dtype。

[T12]

由於陣列種類為整數,因此結果使用預設複數精度,即 complex128

動機和範疇#

變更關於檢查 Python 純量和 NumPy 純量/0-D 陣列的值的行為的動機有三重

  1. NumPy 純量/0-D 陣列的特殊處理以及值檢查可能讓使用者感到非常驚訝,

  2. 值檢查邏輯更難以解釋和實作。透過 NEP 42,也更難以使其適用於使用者定義的 DType。目前,這導致了新系統和舊系統(值敏感系統)的雙重實作。修復此問題將大大簡化內部邏輯,並使結果更加一致。

  3. 它在很大程度上與其他專案(如 JAXdata-apis.org)的選擇一致(另請參閱 相關工作)。

我們相信,「弱」Python 純量的提案將透過為使用者提供清晰的心智模型來幫助使用者,了解運算將產生哪種資料類型。此模型非常適合 NumPy 目前經常遵循的陣列精度保留,也適用於就地運算

arr += value

只要不跨越「種類」邊界,就保留精度(否則會引發錯誤)。

雖然有些使用者可能會懷念值檢查行為,但即使在那些看起來有用的情況下,它也很快會導致驚訝。這可能是預期的

np.array([100], dtype=np.uint8) + 1000 == np.array([1100], dtype=np.uint16)

但以下情況會令人驚訝

np.array([100], dtype=np.uint8) + 200 == np.array([44], dtype=np.uint8)

考慮到該提案與就地運算元的行為一致,並避免了僅有時避免結果溢位的令人驚訝的行為切換,我們相信該提案遵循了「最小驚訝原則」。

使用方式和影響#

預計此 NEP 的實作將沒有針對所有變更發出警告的過渡期。這樣的過渡期會產生許多(通常是無害的)警告,這些警告將難以靜音。我們預期大多數使用者將長期受益於更清晰的升級規則,並且很少有人會受到此變更的直接(負面)影響。但是,某些使用模式可能會導致有問題的變更,這些在向後相容性章節中詳細說明。

此問題的解決方案將是能夠通知使用者潛在行為變更的可選警告模式。此模式預計會產生許多無害的警告,但提供了一種系統地審查程式碼並在觀察到問題時追蹤變更的方法。

can_cast 的影響#

can_cast 將永遠不再檢查值。因此,以下結果預計會從 True 變更為 False

np.can_cast(np.int64(100), np.uint8)
np.can_cast(np.array(100, dtype=np.int64), np.uint8)
np.can_cast(100, np.uint8)

我們預期此變更的影響與以下變更的影響相比將很小。

注意

最後一個範例,其中輸入是 Python 純量,可能 會被保留,因為 100 可以用 uint8 表示。

對涉及 NumPy 陣列或純量的運算符和函數的影響#

不涉及 Python 純量(floatintcomplex)的運算的主要影響將是,對 0-D 陣列和 NumPy 純量的運算將永遠不會依賴它們的值。這消除了目前令人驚訝的情況。例如

np.arange(10, dtype=np.uint8) + np.int64(1)
# and:
np.add(np.arange(10, dtype=np.uint8), np.int64(1))

未來將傳回 int64 陣列,因為 np.int64(1) 的類型會被嚴格遵守。目前會傳回 uint8 陣列。

對涉及 Python intfloatcomplex 的運算符的影響#

此 NEP 嘗試在使用文字值時保留舊行為的便利性。當涉及「未類型化」的文字 Python 純量時,目前基於值的邏輯具有一些不錯的屬性

np.arange(10, dtype=np.int8) + 1  # returns an int8 array
np.array([1., 2.], dtype=np.float32) * 3.5  # returns a float32 array

但在遇到「無法表示」的值時會導致驚訝

np.arange(10, dtype=np.int8) + 256  # returns int16
np.array([1., 2.], dtype=np.float32) * 1e200  # returns float64

該提案是在很大程度上保留此行為。這是透過將 Python intfloatcomplex 視為運算中的「弱」類型來實現的。但是,為了避免驚訝,我們計畫使轉換為新類型更加嚴格:在前兩個範例中,結果將保持不變,但在第二個範例中,它將以以下方式變更

np.arange(10, dtype=np.int8) + 256  # raises a TypeError
np.array([1., 2.], dtype=np.float32) * 1e200  # warning and returns infinity

第二個範例會發出警告,因為 np.float32(1e200) 溢位到無限大。然後它將繼續像往常一樣使用 inf 進行計算。

其他程式庫中的行為

在轉換中溢位而不是引發錯誤是一種選擇;這是大多數 C 設定中的預設值(與 NumPy C 類似,可以設定為由於溢位而引發錯誤)。例如,這也是 pytorch 1.10 的行為。

Python 整數的特殊行為#

NEP 的升級規則以結果 dtype 表示,結果 dtype 通常也是運算 dtype(就結果精度而言)。這導致了 Python 整數的例外情況:雖然 uint8(3) + 1000 必須被拒絕,因為在 uint8 中運算是不可能的,但 uint8(3) / 1000 會傳回 float64,並且可以將兩個輸入都轉換為 float64 以找到結果。

實際上,這表示在以下情況下接受任意 Python 整數值

  • NumPy 和 Python 整數之間的所有比較(==< 等)始終定義明確。

  • np.sqrt 這樣產生浮點結果的一元函數可以並且將會將 Python 整數轉換為浮點數。

  • 整數除法透過將輸入轉換為 float64 來傳回浮點數。

請注意,可能還有其他函數可以應用這些例外情況,但實際上並未應用。在這些情況下,允許它們應該被視為一種改進,但當使用者影響較小時,我們可能不會為了簡化而這樣做。

向後相容性#

一般來說,僅使用預設 dtype float64 或 int32/int64 或更精確的 dtype 的程式碼不應受到影響。

但是,提議的變更將在許多混合 0-D 或純量值(具有非預設 dtype)的情況下修改結果。在許多情況下,這些將是錯誤修復,但是,某些變更可能對終端使用者造成問題。

最重要的可能失敗可能是以下範例

arr = np.arange(100, dtype=np.uint8)  # storage array with low precision
value = arr[10]

# calculation continues with "value" without considering where it came from
value * 100

在以前,value * 100 會導致向上轉換為 int32/int64(因為 value 是純量)。除非明確處理(就像 value 是陣列一樣),否則新行為將保留較低的精度。這可能會導致整數溢位,從而導致超出精度的不正確結果。在許多情況下,這可能是靜默的,儘管 NumPy 通常會為純量運算符發出警告。

同樣地,如果儲存陣列是 float32,則計算可能會保留較低的 float32 精度,而不是使用預設的 float64

可能會發生更多問題。例如

  • 在混合精度時,浮點比較(尤其是相等性)可能會變更

    np.float32(1/3) == 1/3  # was False, will be True.
    
  • 預計某些運算會開始失敗

    np.array([1], np.uint8) * 1000
    np.array([1], np.uint8) == 1000  # possibly also
    

    以保護使用者在先前基於值的轉換導致向上轉換的情況下。(當將 1000 轉換為 uint8 時會發生失敗。)

  • 浮點溢位可能發生在更奇怪的情況下

    np.float32(1e-30) * 1e50  # will return ``inf`` and a warning
    

    因為 np.float32(1e50) 傳回 inf。以前,即使 1e50 不是 0-D 陣列,這也會傳回雙精度結果

在其他情況下,可能會發生精度提高。例如

np.multiple(float32_arr, 2.)
float32_arr * np.float64(2.)

兩者都將傳回 float64 而不是 float32。這提高了精度,但稍微變更了結果並使用了雙倍的記憶體。

由於整數「精度階梯」而發生的變更#

當從 Python 整數建立陣列時,NumPy 將依序嘗試以下類型,結果取決於值

long (usually int64) → int64 → uint64 -> object

這與上面描述的升級略有不同。

此 NEP 目前不包括變更此階梯(儘管可能會在單獨的文件中建議)。但是,在混合運算中,此階梯將被忽略,因為該值將被忽略。這表示運算永遠不會靜默地使用 object dtype

np.array([3]) + 2**100  # Will error

使用者將必須編寫以下程式碼之一

np.array([3]) + np.array(2**100)
np.array([3]) + np.array(2**100, dtype=object)

由於隱含轉換為 object 應該很少見,並且解決方法很明確,因此我們預期向後相容性問題相當小。

詳細描述#

以下內容提供了關於目前「基於值」的升級邏輯的一些額外詳細資訊,然後是關於「弱純量」升級及其內部處理方式的詳細資訊。

「基於值」升級的舊實作#

本節回顧了目前基於值的邏輯在實務中是如何運作的,請參閱以下章節中的範例,了解它如何有用。

當 NumPy 看到「純量」值時,它可以是 Python int、float、complex、NumPy 純量或陣列

1000  # Python scalar
int32(1000)  # NumPy scalar
np.array(1000, dtype=int64)  # zero dimensional

或者對於 float/complex 等效類型,NumPy 會忽略 dtype 的精度,並找到可以容納該值的最小可能 dtype。也就是說,它會嘗試以下 dtype

  • 整數:uint8int8uint16int16uint32int32uint64int64

  • 浮點數:float16float32float64longdouble

  • 複數:complex64complex128clongdouble

請注意,例如對於整數值 10,最小的 dtype *可以* 是 uint8int8

當所有參數都是純量值時,NumPy 從未應用此規則

np.int64(1) + np.int32(2) == np.int64(3)

對於整數,一個值是否適合完全取決於它是否可以由 dtype 表示。對於浮點數和複數,如果滿足以下條件,則認為 dtype 足夠:

  • float16-65000 < value < 65000(或 NaN/Inf)

  • float32-3.4e38 < value < 3.4e38(或 NaN/Inf)

  • float64-1.7e308 < value < 1.7e308(或 Nan/Inf)

  • longdouble:(最大範圍,因此沒有限制)

對於複數,這些邊界適用於實部和虛部。這些值大致對應於 np.finfo(np.float32).max。(NumPy 從未強制對 float32(3.402e38) 的值使用 float64,但對於 Python 值 3.402e38 則會。)

目前「基於值」提升的狀態#

在我們可以提出目前資料類型系統的替代方案之前,回顧「基於值的提升」如何使用以及如何發揮作用是有幫助的。基於值的提升允許以下程式碼運作:

# Create uint8 array, as this is sufficient:
uint8_arr = np.array([1, 2, 3], dtype=np.uint8)
result = uint8_arr + 4
result.dtype == np.uint8

result = uint8_arr * (-1)
result.dtype == np.int16  # upcast as little as possible.

其中第一部分尤其有用:使用者知道輸入是一個具有特定精度的整數陣列。考慮到簡單的 + 4 保留先前的資料類型是很直觀的。用 np.float32 替換這個範例可能更清楚,因為浮點數很少會溢位。如果沒有這種行為,上面的範例將需要寫成 np.uint8(4),並且缺乏這種行為會使以下情況令人驚訝:

result = np.array([1, 2, 3], dtype=np.float32) * 2.
result.dtype == np.float32

在沒有特殊情況的情況下,會導致返回 float64

重要的是要注意,這種行為也適用於通用函式和零維陣列

# This logic is also used for ufuncs:
np.add(uint8_arr, 4).dtype == np.uint8
# And even if the other array is explicitly typed:
np.add(uint8_arr, np.array(4, dtype=np.int64)).dtype == np.uint8

為了回顧,如果我們將 4 替換為 [4] 使其成為一維,則結果將會不同

# This logic is also used for ufuncs:
np.add(uint8_arr, [4]).dtype == np.int64  # platform dependent
# And even if the other array is explicitly typed:
np.add(uint8_arr, np.array([4], dtype=np.int64)).dtype == np.int64

建議的弱式提升#

此提案使用「弱式純量」邏輯。這表示 Python 的 intfloatcomplex 不會被分配典型的 dtype 之一,例如 float64 或 int64。相反,它們被分配一個特殊的抽象 DType,類似於「純量」階層名稱:Integral、Floating、ComplexFloating。

當發生提升時(就像 ufuncs 在沒有完全匹配的迴圈時所做的那樣),另一個 DType 能夠決定如何看待 Python 純量。例如,UInt16Integral 提升將會得到 UInt16

注意

未來很可能會為使用者定義的 DType 提供預設值。最有可能的情況是最終成為預設的整數/浮點數,但原則上可以實作更複雜的方案。

在任何時候都不會使用值來決定此提升的結果。僅當值轉換為新的 dtype 時才會考慮該值;這可能會引發錯誤。

實作#

實作此 NEP 需要在所有二元運算子(或 ufuncs)中新增一些額外的機制,以便它們在可能的情況下嘗試使用「弱式」邏輯。有兩種可能的方法:

  1. 二元運算子只是嘗試在出現這種情況時呼叫 np.result_type(),並將 Python 純量轉換為結果類型(如果已定義)。

  2. 二元運算子指示輸入是 Python 純量,並且 ufunc 分派/提升機制用於其餘部分(請參閱 NEP 42)。這允許更大的彈性,但需要在 ufunc 機制中新增一些額外的邏輯。

注意

到目前為止,尚不清楚哪種方法更好,任何一種方法都會給出相當等效的結果,並且 1. 可以在未來根據需要透過 2. 進行擴展。

它還需要移除所有目前基於值的特殊程式碼路徑。

令人費解的是,實作中較大的一步可能是實作一個解決方案,以便在以下範例中引發錯誤:

np.arange(10, dtype=np.uint8) + 1000

即使 np.uint8(1000) 返回的值與 np.uint8(232) 相同。

注意

請參閱替代方案,我們可能尚未決定這種靜默溢位是可以接受的,或者至少是一個單獨的問題。

替代方案#

在幾個設計軸向上,可以有不同的選擇。以下章節概述了這些選擇。

使用強型別純量或兩者混合使用#

解決基於值的提升/轉換問題的最簡單方法是使用強型別 Python 純量,即 Python 浮點數被視為雙精度,而 Python 整數始終被視為與預設整數 dtype 相同。

這將是最簡單的解決方案,但是,當使用 float32int16 等陣列時,會導致許多向上轉換。這些情況的解決方案是依賴就地運算。我們目前認為,雖然危險性較低,但此變更會影響許多使用者,並且會比不變更更常令人感到意外(儘管期望差異很大)。

原則上,弱式與強式行為不必一致。也可以使 Python 浮點數使用弱式行為,但 Python 整數使用強式行為,因為整數溢位更令人驚訝。

不要在函式中使用弱純量邏輯#

此 NEP 提案的一個替代方案是將弱型別的使用範圍縮小到 Python 運算子。

這有優點和缺點

  • 主要優點是將其限制在 Python 運算子意味著這些「弱式」型別/dtype 明顯是短暫的 Python 陳述式。

  • 缺點是 np.multiply* 的互換性較差。

  • 僅針對運算子使用「弱式」提升意味著程式庫不必擔心它們是否要「記住」輸入最初是 Python 純量。另一方面,它會為 Python 運算子增加稍微不同(或額外)的邏輯需求。(技術上來說,可能是作為 ufunc 分派機制的一個標誌,用於切換弱式邏輯。)

  • __array_ufunc__ 通常單獨使用,為實作它的類陣列提供 Python 運算子支援。如果運算子很特殊,這些類陣列可能需要一種機制來匹配 NumPy(例如,ufuncs 的 kwarg 來啟用弱式提升。)

NumPy 純量可能很特殊#

許多使用者期望 NumPy 純量應與 NumPy 陣列不同,因為 np.uint8(3) + 3 應返回 int64(或 Python 整數),而 uint8_arr + 3 保留 uint8 dtype。

此替代方案將非常接近 NumPy 純量的目前行為,但它將鞏固陣列和純量之間的區別(NumPy 陣列比 Python 純量「更強」,但 NumPy 純量則不然)。

這種區別在很大程度上是可能的,但是,此時 NumPy 通常會(並且靜默地)將 0-D 陣列轉換為純量。因此,如果我們也改變這種靜默轉換(有時稱為「衰減」)行為,則僅考慮此替代方案可能才有意義。

處理純量在不安全時的轉換#

諸如以下情況:

np.arange(10, dtype=np.uint8) + 1000

應根據此 NEP 引發錯誤。可以放寬為給出警告,甚至忽略「不安全」轉換,這(在所有相關硬體上)會導致使用 np.uint8(1000) == np.uint8(232)

允許弱型別陣列#

擁有弱型別 Python 純量,但沒有弱型別陣列的一個問題是,在許多情況下,會不加區別地在輸入上呼叫 np.asarray()。為了解決這個問題,JAX 將 np.asarray(1) 的結果也視為弱型別。然而,這有兩個難處:

  1. JAX 注意到以下情況可能會令人困惑:

    np.broadcast_to(np.asarray(1), (100, 100))
    

    是一個非 0-D 陣列,它「繼承」了弱型別。[2]

  2. 與 JAX 張量不同,NumPy 陣列是可變的,因此賦值可能需要使其成為強型別?

標誌可能作為實作細節很有用(例如,在 ufuncs 中),但是,到目前為止,我們不希望將其作為使用者 API。主要原因是,如果此類標誌作為函式的結果傳遞出去,而不是僅在非常局部的位置使用,則可能會讓使用者感到驚訝。

待辦事項

在接受 NEP 之前,可能最好進一步討論此問題。程式庫可能需要更清晰的模式來「傳播」「弱式」型別,這可能只是一個 np.asarray_or_literal() 來保留 Python 純量,或者是在 np.asarray() 之前呼叫 np.result_type() 的模式。

繼續對 Python 純量使用基於值的邏輯#

目前邏輯的一些主要問題出現的原因是,我們將其應用於 NumPy 純量和 0-D 陣列,而不是應用於 Python 純量。因此,我們可以考慮繼續檢查 Python 純量的值。

我們拒絕這個想法,理由是它不會消除先前給出的意外情況

np.uint8(100) + 1000 == np.uint16(1100)
np.uint8(100) + 200 == np.uint8(44)

並且基於結果值而不是輸入值來調整精度對於純量運算可能是可行的,但對於陣列運算是不可行的。這是因為陣列運算需要在執行計算之前分配結果陣列。

討論#

參考文獻與註腳#