NEP 5 — 廣義通用函數#

狀態:

最終版本

對於不僅對純量函數進行迴圈,也對向量(或陣列)函數進行迴圈存在普遍需求,如 http://scipy.org/scipy/numpy/wiki/GeneralLoopingFunctions 中所述。我們建議透過廣義化通用函數 (ufuncs) 來實現此概念,並提供一個 C 語言實作,該實作向 numpy 程式碼庫新增約 500 行程式碼。在目前的(專用)ufuncs 中,基本函數僅限於逐元素操作,而廣義版本支援「子陣列」對「子陣列」操作。Perl 向量程式庫 PDL 提供了類似的功能,以下重新使用了其術語。

每個廣義 ufunc 都具有與之相關聯的資訊,該資訊說明輸入的「核心」維度,以及對應輸出的維度(逐元素 ufuncs 具有零核心維度)。所有引數的核心維度列表稱為 ufunc 的「簽名」。例如,ufunc numpy.add 的簽名為 (),()->(),定義了兩個純量輸入和一個純量輸出。

另一個範例是(請參閱 GeneralLoopingFunctions 頁面)簽名為 (i),(i)->() 的函數 inner1d(a,b)。這會沿著每個輸入的最後一個軸套用內積,但保持其餘索引不變。例如,當 a 的形狀為 (3,5,N)b 的形狀為 (5,N) 時,這將傳回形狀為 (3,5) 的輸出。底層的基本函數會被呼叫 3*5 次。在簽名中,我們為每個輸入指定一個核心維度 (i),為輸出指定零個核心維度 (),因為它接受兩個 1 維陣列並傳回純量值。透過使用相同的名稱 i,我們指定兩個對應的維度應具有相同的大小(或者其中一個維度的大小為 1 並將被廣播)。

超出核心維度的維度稱為「迴圈」維度。在上面的範例中,這對應於 (3,5)

常用的 numpy「廣播」規則適用,其中簽名決定了每個輸入/輸出物件的維度如何分割為核心維度和迴圈維度

  1. 當輸入陣列的維度小於對應的核心維度數量時,會在形狀前面加上 1。

  2. 核心維度會從所有輸入中移除,剩餘的維度會被廣播;定義迴圈維度。

  3. 輸出由迴圈維度加上輸出核心維度給出。

定義#

基本函數

每個 ufunc 都包含一個基本函數,該函數對陣列引數的最小部分執行最基本的操作(例如,將兩個數字相加是將兩個陣列相加的最基本操作)。ufunc 在陣列的不同部分多次套用基本函數。基本函數的輸入/輸出可以是向量;例如,inner1d 的基本函數接受兩個向量作為輸入。

簽名

簽名是一個字串,描述 ufunc 的基本函數的輸入/輸出維度。請參閱下面的章節以了解更多詳細資訊。

核心維度

基本函數的每個輸入/輸出的維度由其核心維度定義(零核心維度對應於純量輸入/輸出)。核心維度會對應到輸入/輸出陣列的最後維度。

維度名稱

維度名稱代表簽名中的核心維度。不同的維度可以共用一個名稱,表示它們的大小相同(或可廣播)。

維度索引

維度索引是一個整數,代表維度名稱。它根據每個名稱在簽名中首次出現的順序來列舉維度名稱。

簽名詳細資訊#

簽名定義了輸入和輸出變數的「核心」維度,並由此定義了維度的縮併。簽名由以下格式的字串表示

  • 每個輸入或輸出陣列的核心維度由括號中的維度名稱列表表示,(i_1,...,i_N);純量輸入/輸出以 () 表示。您可以使用任何有效的 Python 變數名稱來代替 i_1i_2 等。

  • 不同引數的維度列表以 "," 分隔。輸入/輸出引數以 "->" 分隔。

  • 如果在多個位置使用相同的維度名稱,則會強制對應維度的大小相同(或可廣播的大小)。

簽名的正式語法如下

<Signature>            ::= <Input arguments> "->" <Output arguments>
<Input arguments>      ::= <Argument list>
<Output arguments>     ::= <Argument list>
<Argument list>        ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument>             ::= "(" <Core dimension list> ")"
<Core dimension list>  ::= nil | <Dimension name> |
                           <Dimension name> "," <Core dimension list>
<Dimension name>       ::= valid Python variable name

注意事項

  1. 所有引號僅為清晰起見。

  2. 共用相同名稱的核心維度必須是可廣播的,就像我們上面範例中的兩個 i 一樣。每個維度名稱通常對應於基本函數實作中的一個迴圈層級。

  3. 空白字元會被忽略。

以下是一些簽名範例

add

(),()->()

inner1d

(i),(i)->()

sum1d

(i)->()

dot2d

(m,n),(n,p)->(m,p)

矩陣乘法

outer_inner

(i,t),(j,t)->(i,j)

最後一個維度的內積,倒數第二個維度的外積,以及其餘維度的迴圈/廣播。

用於實作基本函數的 C-API#

目前的介面保持不變,PyUFunc_FromFuncAndData 仍然可以用於實作(專用)ufuncs,其由純量基本函數組成。

可以使用 PyUFunc_FromFuncAndDataAndSignature 來宣告更通用的 ufunc。PyUFunc_FromFuncAndDataAndSignature 的引數列表與 PyUFunc_FromFuncAndData 相同,但額外有一個引數將簽名指定為 C 字串。

此外,回呼函數的型別與之前相同,void (*foo)(char **args, intp *dimensions, intp *steps, void *func)。當被調用時,args 是長度為 nargs 的列表,其中包含所有輸入/輸出引數的資料。對於純量基本函數,steps 的長度也為 nargs,表示用於引數的步幅。dimensions 是指向單一整數的指標,該整數定義要迴圈的軸的大小。

對於非平凡簽名,dimensions 也將包含核心維度的大小,從第二個條目開始。每個唯一維度名稱僅提供一個大小,並且大小會根據維度名稱在簽名中首次出現的順序給出。

steps 的前 nargs 個元素與純量 ufuncs 保持相同。後續元素包含所有引數的所有核心維度的步幅,依序排列。

例如,考慮簽名為 (i,j),(i)->() 的 ufunc。在這種情況下,args 將包含三個指向輸入/輸出陣列 abc 資料的指標。此外,dimensions 將為 [N, I, J],以定義迴圈大小 N 以及核心維度 ij 的大小 IJ。最後,steps 將為 [a_N, b_N, c_N, a_i, a_j, b_i],其中包含所有必要的步幅。