通用函數 (ufunc) 基礎知識#

通用函數(或簡稱 ufunc)是一種函數,它以元素對元素的方式對 ndarrays 進行操作,支援陣列廣播類型轉換 和其他幾種標準功能。也就是說,ufunc 是一個「向量化」包裝函式,用於接收固定數量的特定輸入並產生固定數量的特定輸出的函數。

在 NumPy 中,通用函數是 numpy.ufunc 類別的實例。許多內建函數都是在編譯過的 C 程式碼中實作的。基本 ufunc 對純量進行操作,但也有一種廣義的 ufunc,其基本元素是子陣列(向量、矩陣等),並且廣播是在其他維度上完成的。最簡單的例子是加法運算子

>>> np.array([0,2,3,4]) + np.array([1,1,-1,2])
array([1, 3, 2, 6])

人們也可以使用 numpy.frompyfunc 工廠函數產生自訂的 numpy.ufunc 實例。

Ufunc 方法#

所有 ufunc 都有四種方法。它們可以在 方法 中找到。但是,這些方法僅對接收兩個輸入引數並返回一個輸出引數的純量 ufunc 有意義。嘗試在其他 ufunc 上呼叫這些方法將導致 ValueError

類似 reduce 的方法都採用 axis 關鍵字、dtype 關鍵字和 out 關鍵字,並且陣列都必須具有 >= 1 的維度。axis 關鍵字指定陣列的軸,將在該軸上進行縮減(負值向後計數)。通常,它是一個整數,但對於 numpy.ufunc.reduce,它也可以是 int 的元組,以一次縮減多個軸,或者 None,以縮減所有軸。例如

>>> x = np.arange(9).reshape(3,3)
>>> x
array([[0, 1, 2],
      [3, 4, 5],
      [6, 7, 8]])
>>> np.add.reduce(x, 1)
array([ 3, 12, 21])
>>> np.add.reduce(x, (0, 1))
36

dtype 關鍵字允許您管理在使用 ufunc.reduce 時天真地出現的一個非常常見的問題。有時您可能有一個特定資料類型的陣列,並希望將其所有元素加總,但結果不適合陣列的資料類型。如果您有一個單位元組整數陣列,這種情況通常會發生。dtype 關鍵字允許您更改縮減發生的資料類型(以及因此輸出的類型)。因此,您可以確保輸出是一種精度足夠大以處理輸出的資料類型。更改縮減類型的責任主要由您承擔。有一個例外:如果未為「add」或「multiply」運算上的縮減指定 dtype,則如果輸入類型是整數(或布林值)資料類型且小於 numpy.int_ 資料類型的大小,則會在內部向上轉換為 int_(或 numpy.uint)資料類型。在先前的範例中

>>> x.dtype
dtype('int64')
>>> np.multiply.reduce(x, dtype=float)
array([ 0., 28., 80.])

最後,out 關鍵字允許您提供輸出陣列(或多輸出 ufunc 的輸出陣列元組)。如果給定 out,則 dtype 引數僅用於內部計算。考慮先前範例中的 x

>>> y = np.zeros(3, dtype=int)
>>> y
array([0, 0, 0])
>>> np.multiply.reduce(x, dtype=float, out=y)
array([ 0, 28, 80])

Ufunc 還有第五種方法,numpy.ufunc.at,它允許使用進階索引執行原地操作。在使用進階索引的維度上不使用 緩衝,因此進階索引可以多次列出一個項目,並且該操作將針對該項目的先前操作結果執行。

輸出類型判斷#

如果所有輸入引數都不是 ndarray,則 ufunc(及其方法)的輸出不一定是 ndarray。實際上,如果任何輸入定義了 __array_ufunc__ 方法,則控制權將完全傳遞給該函數,也就是說,ufunc 被覆寫

如果沒有任何輸入覆寫 ufunc,則所有輸出陣列都將傳遞到定義它的輸入(除了 ndarrays 和純量)的 __array_wrap__ 方法,並且具有任何其他通用函數輸入的最高 __array_priority__。ndarray 的預設 __array_priority__ 為 0.0,而子類型的預設 __array_priority__ 為 0.0。矩陣的 __array_priority__ 等於 10.0。

所有 ufunc 也可以採用必須是陣列或子類別的輸出引數。如有必要,結果將轉換為提供的輸出陣列的資料類型。如果輸出具有 __array_wrap__ 方法,則會呼叫它,而不是在輸入上找到的方法。

廣播#

每個通用函數都採用陣列輸入並產生陣列輸出,方法是對輸入執行元素對元素的運算核心函數(其中元素通常是純量,但對於廣義 ufunc 而言,可以是向量或更高階的子陣列)。套用標準的 廣播規則,以便仍然可以對形狀不完全相同的輸入進行有用的運算。

根據這些規則,如果輸入在其形狀中的維度大小為 1,則該維度中的第一個資料條目將用於沿該維度的所有計算。換句話說,ufunc 的步進機制將不會沿該維度步進(該維度的 步幅 將為 0)。

類型轉換規則#

注意

在 NumPy 1.6.0 中,建立了一個類型提升 API,以封裝用於判斷輸出類型的機制。有關更多詳細資訊,請參閱函數 numpy.result_typenumpy.promote_typesnumpy.min_scalar_type

每個 ufunc 的核心都是一維步進迴圈,它為特定的類型組合實作實際函數。建立 ufunc 時,會為其提供內部迴圈的靜態清單以及 ufunc 在其上運作的類型簽章的對應清單。ufunc 機制使用此清單來判斷要用於特定案例的內部迴圈。您可以檢查特定 ufunc 的 .types 屬性,以查看哪些類型組合具有已定義的內部迴圈以及它們產生的輸出類型(字元碼 用於上述輸出以求簡潔)。

每當 ufunc 沒有為提供的輸入類型實作核心迴圈時,都必須對一個或多個輸入執行轉換。如果找不到輸入類型的實作,則演算法會搜尋具有類型簽章的實作,所有輸入都可以「安全地」轉換為該類型簽章。它在其內部迴圈清單中找到的第一個迴圈會被選取並執行,在執行所有必要的類型轉換之後。回想一下,ufunc 期間的內部副本(即使對於轉換也是如此)都限制為內部緩衝區的大小(使用者可設定)。

注意

NumPy 中的通用函數非常靈活,可以具有混合類型簽章。因此,例如,可以定義一個通用函數,使其適用於浮點值和整數值。請參閱 numpy.ldexp 以取得範例。

根據上述描述,轉換規則基本上是透過資料類型何時可以「安全地」轉換為另一種資料類型的問題來實作的。這個問題的答案可以在 Python 中使用函數呼叫來判斷:can_cast(fromtype, totype)。下面的範例顯示了在作者的 64 位元系統上,針對 24 種內部支援類型進行此呼叫的結果。您可以使用範例中給出的程式碼為您的系統產生此表格。

範例

程式碼片段顯示了 64 位元系統的「可以安全轉換」表格。一般來說,輸出取決於系統;您的系統可能會產生不同的表格。

>>> mark = {False: ' -', True: ' Y'}
>>> def print_table(ntypes):
...     print('X ' + ' '.join(ntypes))
...     for row in ntypes:
...         print(row, end='')
...         for col in ntypes:
...             print(mark[np.can_cast(row, col)], end='')
...         print()
...
>>> print_table(np.typecodes['All'])
X ? b h i l q n p B H I L Q N P e f d g F D G S U V O M m
? Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
b - Y Y Y Y Y Y Y - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - Y
h - - Y Y Y Y Y Y - - - - - - - - Y Y Y Y Y Y Y Y Y Y - Y
i - - - Y Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
l - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
q - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
n - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
p - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
B - - Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
H - - - Y Y Y Y Y - Y Y Y Y Y Y - Y Y Y Y Y Y Y Y Y Y - Y
I - - - - Y Y Y Y - - Y Y Y Y Y - - Y Y - Y Y Y Y Y Y - Y
L - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
Q - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
N - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
P - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
e - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - -
f - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y - -
d - - - - - - - - - - - - - - - - - Y Y - Y Y Y Y Y Y - -
g - - - - - - - - - - - - - - - - - - Y - - Y Y Y Y Y - -
F - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y - -
D - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y - -
G - - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y - -
S - - - - - - - - - - - - - - - - - - - - - - Y Y Y Y - -
U - - - - - - - - - - - - - - - - - - - - - - - Y Y Y - -
V - - - - - - - - - - - - - - - - - - - - - - - - Y Y - -
O - - - - - - - - - - - - - - - - - - - - - - - - - Y - -
M - - - - - - - - - - - - - - - - - - - - - - - - Y Y Y -
m - - - - - - - - - - - - - - - - - - - - - - - - Y Y - Y

您應該注意,雖然為了完整性而包含在表格中,但 ufunc 無法對 'S'、'U' 和 'V' 類型進行運算。另請注意,在 32 位元系統上,整數類型可能具有不同的大小,從而導致表格略有不同。

混合純量陣列運算使用一組不同的轉換規則,這些規則確保純量不會「向上轉換」陣列,除非純量與陣列的資料類型在根本上不同(即,在資料類型階層中處於不同的階層)。此規則使您可以在程式碼中使用純量常數(作為 Python 類型,在 ufunc 中會相應地解釋),而無需擔心純量常數的精度是否會導致對您的大型(小精度)陣列進行向上轉換。

內部緩衝區的使用#

在內部,緩衝區用於未對齊的資料、交換的資料以及必須從一種資料類型轉換為另一種資料類型的資料。內部緩衝區的大小是按執行緒設定的。最多可以建立 \(2 (n_{\mathrm{inputs}} + n_{\mathrm{outputs}})\) 個指定大小的緩衝區,以處理來自 ufunc 的所有輸入和輸出的資料。緩衝區的預設大小為 10,000 個元素。每當需要基於緩衝區的計算時,但所有輸入陣列都小於緩衝區大小,則會在計算繼續之前複製那些行為不當或類型不正確的陣列。因此,調整緩衝區的大小可能會改變完成各種 ufunc 計算的速度。可以使用函數 numpy.setbufsize 存取用於設定此變數的簡單介面。

錯誤處理#

通用函數可能會觸發硬體中的特殊浮點狀態暫存器(例如除以零)。如果在您的平台上可用,則會在計算期間定期檢查這些暫存器。錯誤處理是按執行緒控制的,可以使用函數 numpy.seterrnumpy.seterrcall 進行配置。

覆寫 ufunc 行為#

類別(包括 ndarray 子類別)可以透過定義某些特殊方法來覆寫 ufunc 對它們的作用方式。有關詳細資訊,請參閱 標準陣列子類別