超越基礎#

發現之旅不在於尋找新的風景,而在於擁有
新的眼光。
馬塞爾·普魯斯特
發現是看到每個人都看到的東西,並思考沒有
人想過的東西。
艾伯特·聖捷爾吉

迭代陣列中的元素#

基本迭代#

一個常見的演算法需求是能夠遍歷多維陣列中的所有元素。陣列迭代器物件使得以通用方式執行此操作變得容易,適用於任何維度的陣列。當然,如果您知道您將使用的維度數量,那麼您始終可以編寫巢狀 for 迴圈來完成迭代。但是,如果您想編寫適用於任何維度數量的程式碼,那麼您可以使用陣列迭代器。當存取陣列的 .flat 屬性時,會傳回陣列迭代器物件。

基本用法是呼叫 PyArray_IterNew ( array ),其中 array 是一個 ndarray 物件(或其子類別之一)。傳回的物件是一個陣列迭代器物件(與 ndarray 的 .flat 屬性傳回的物件相同)。此物件通常會被轉換為 PyArrayIterObject*,以便可以存取其成員。唯一需要的成員是 iter->size,其中包含陣列的總大小;iter->index,其中包含陣列的目前一維索引;以及 iter->dataptr,它是指向陣列目前元素資料的指標。有時,存取 iter->ao 也很有用,它是指向底層 ndarray 物件的指標。

在處理完陣列目前元素的資料後,可以使用巨集 PyArray_ITER_NEXT ( iter ) 取得陣列的下一個元素。迭代始終以 C 風格的連續方式進行(最後一個索引變化最快)。PyArray_ITER_GOTO ( iter , destination ) 可用於跳轉到陣列中的特定點,其中 destination 是一個 npy_intp 資料類型的陣列,其空間足以處理底層陣列中的至少維度數量。有時,使用 PyArray_ITER_GOTO1D ( iter , index ) 很有用,它將跳轉到由 index 值給定的一維索引。然而,最常見的用法在以下範例中給出。

PyObject *obj; /* assumed to be some ndarray object */
PyArrayIterObject *iter;
...
iter = (PyArrayIterObject *)PyArray_IterNew(obj);
if (iter == NULL) goto fail;   /* Assume fail has clean-up code */
while (iter->index < iter->size) {
    /* do something with the data at it->dataptr */
    PyArray_ITER_NEXT(it);
}
...

您也可以使用 PyArrayIter_Check ( obj ) 來確保您擁有迭代器物件,並使用 PyArray_ITER_RESET ( iter ) 將迭代器物件重設回陣列的開頭。

應該在此強調,如果您的陣列已經是連續的,您可能不需要陣列迭代器(使用陣列迭代器可以工作,但會比您可以編寫的最快程式碼慢)。陣列迭代器的主要目的是封裝對具有任意步幅的 N 維陣列的迭代。它們在 NumPy 原始碼本身的許多地方被使用。如果您已經知道您的陣列是連續的(Fortran 或 C),那麼只需將元素大小添加到正在運行的指標變數中,即可非常有效地逐步遍歷陣列。換句話說,在連續情況下(假設為雙精度浮點數),像這樣的程式碼可能會更快。

npy_intp size;
double *dptr;  /* could make this any variable type */
size = PyArray_SIZE(obj);
dptr = PyArray_DATA(obj);
while(size--) {
   /* do something with the data at dptr */
   dptr++;
}

迭代除了單一軸之外的所有軸#

一個常見的演算法是迴圈遍歷陣列的所有元素,並透過發出函數呼叫對每個元素執行某些函數。由於函數呼叫可能很耗時,因此加速此類演算法的一種方法是編寫函數,使其接受資料向量,然後編寫迭代,以便一次對整個資料維度執行函數呼叫。這增加了每次函數呼叫完成的工作量,從而將函數呼叫的額外負擔減少到總時間的一小部分(或更小)。即使迴圈內部在沒有函數呼叫的情況下執行,對具有最多元素數量的維度執行內部迴圈也可能是有利的,以利用微處理器上可用的速度增強功能,這些微處理器使用管線來增強基本運算。

PyArray_IterAllButAxis ( array , &dim ) 建構一個迭代器物件,該物件經過修改,使其不會迭代由 dim 指示的維度。此迭代器物件的唯一限制是不能使用 PyArray_ITER_GOTO1D ( it , ind ) 巨集(因此,如果您將此物件傳回 Python,平面索引也不會起作用 — 因此您不應該這樣做)。請注意,從此例程傳回的物件通常仍然會轉換為 PyArrayIterObject *。所做的只是修改傳回的迭代器的步幅和維度,以模擬迭代 array[…,0,…],其中 0 放置在 \(\textrm{dim}^{\textrm{th}}\) 維度上。如果 dim 為負數,則會找到並使用具有最大軸的維度。

迭代多個陣列#

通常,希望同時迭代多個陣列。通用函數就是這種行為的一個範例。如果您只想迭代形狀相同的陣列,那麼簡單地建立多個迭代器物件是標準程序。例如,以下程式碼迭代兩個假定具有相同形狀和大小的陣列(實際上,obj1 只需要具有至少與 obj2 一樣多的總元素)。

/* It is already assumed that obj1 and obj2
   are ndarrays of the same shape and size.
*/
iter1 = (PyArrayIterObject *)PyArray_IterNew(obj1);
if (iter1 == NULL) goto fail;
iter2 = (PyArrayIterObject *)PyArray_IterNew(obj2);
if (iter2 == NULL) goto fail;  /* assume iter1 is DECREF'd at fail */
while (iter2->index < iter2->size)  {
    /* process with iter1->dataptr and iter2->dataptr */
    PyArray_ITER_NEXT(iter1);
    PyArray_ITER_NEXT(iter2);
}

廣播多個陣列#

當多個陣列參與運算時,您可能希望使用與數學運算(即 ufunc)相同的廣播規則。這可以使用 PyArrayMultiIterObject 輕鬆完成。這是從 Python 命令 numpy.broadcast 傳回的物件,並且從 C 語言中使用它幾乎同樣容易。函數 PyArray_MultiIterNew ( n , ... ) 被使用(其中 n 個輸入物件代替 ... )。輸入物件可以是陣列或任何可以轉換為陣列的東西。傳回指向 PyArrayMultiIterObject 的指標。廣播已經完成,它調整了迭代器,以便在每個陣列中前進到下一個元素所需做的就是為每個輸入呼叫 PyArray_ITER_NEXT。此遞增由 PyArray_MultiIter_NEXT ( obj ) 巨集自動執行(它可以將多迭代器 obj 作為 PyArrayMultiIterObject*PyObject* 處理)。來自輸入編號 i 的資料可以使用 PyArray_MultiIter_DATA ( obj , i ) 取得。以下是一個使用此功能的範例。

mobj = PyArray_MultiIterNew(2, obj1, obj2);
size = mobj->size;
while(size--) {
    ptr1 = PyArray_MultiIter_DATA(mobj, 0);
    ptr2 = PyArray_MultiIter_DATA(mobj, 1);
    /* code using contents of ptr1 and ptr2 */
    PyArray_MultiIter_NEXT(mobj);
}

函數 PyArray_RemoveSmallest ( multi ) 可用於取得多迭代器物件並調整所有迭代器,以便迭代不會在最大維度上發生(它使該維度的大小為 1)。正在迴圈遍歷的程式碼使用指標,很可能也需要每個迭代器的步幅資料。此資訊儲存在 multi->iters[i]->strides 中。

在 NumPy 原始碼中有幾個使用多迭代器的範例,因為它使 N 維廣播程式碼的編寫非常簡單。瀏覽原始碼以取得更多範例。

使用者定義的資料類型#

NumPy 附帶 24 種內建資料類型。雖然這涵蓋了絕大多數可能的用例,但可以想像使用者可能需要額外的資料類型。NumPy 系統中對新增額外資料類型提供了一些支援。此額外資料類型的功能將與常規資料類型非常相似,只是 ufunc 必須註冊一維迴圈才能單獨處理它。此外,檢查其他資料類型是否可以「安全地」轉換為和從這種新類型轉換,除非您還註冊了新資料類型可以轉換為和從哪些類型轉換,否則將始終傳回「可以轉換」。

NumPy 原始碼包含一個自訂資料類型的範例,作為其測試套件的一部分。原始碼目錄 numpy/_core/src/umath/ 中的檔案 _rational_tests.c.src 包含一個資料類型的實作,該資料類型將有理數表示為兩個 32 位元整數的比率。

新增新的資料類型#

若要開始使用新的資料類型,您需要先定義一個新的 Python 類型來保存新資料類型的純量。如果您的新類型具有二進位相容的佈局,則可以接受從陣列純量之一繼承。這將允許您的新資料類型具有陣列純量的方法和屬性。新的資料類型必須具有固定的記憶體大小(如果您想定義需要彈性表示的資料類型,例如可變精度數字,則使用指向物件的指標作為資料類型)。新 Python 類型的物件結構的記憶體佈局必須是 PyObject_HEAD,後跟資料類型所需的固定大小記憶體。例如,適用於新 Python 類型的結構是

typedef struct {
   PyObject_HEAD;
   some_data_type obval;
   /* the name can be whatever you want */
} PySomeDataTypeObject;

在您定義新的 Python 類型物件後,您必須接著定義新的 PyArray_Descr 結構,其 typeobject 成員將包含指向您剛定義的資料類型的指標。此外,必須在「.f」成員中定義必要的函數:nonzero、copyswap、copyswapn、setItem、getItem 和 cast。但是,您在「.f」成員中定義的函數越多,新資料類型就越有用。將未使用的函數初始化為 NULL 非常重要。這可以使用 PyArray_InitArrFuncs (f) 來實現。

一旦建立新的 PyArray_Descr 結構並填寫了所需的資訊和有用的函數,您就可以呼叫 PyArray_RegisterDataType (new_descr)。此呼叫的傳回值是一個整數,為您提供唯一的 type_number,用於指定您的資料類型。此類型編號應由您的模組儲存和提供,以便其他模組可以使用它來識別您的資料類型。

請注意,此 API 本質上是執行緒不安全的。請參閱 thread_safety 以取得有關 NumPy 中執行緒安全性的更多詳細資訊。

註冊轉換函數#

您可能希望允許將內建(和其他使用者定義的)資料類型自動轉換為您的資料類型。為了使其成為可能,您必須向您希望能夠從中轉換的資料類型註冊轉換函數。這需要為您要支援的每個轉換編寫低階轉換函數,然後向資料類型描述符註冊這些函數。低階轉換函數具有以下簽章。

void castfunc(void *from, void *to, npy_intp n, void *fromarr, void *toarr)#

n 個元素從一種類型轉換為另一種類型。要轉換的資料位於從 from 指向的連續、正確交換和對齊的記憶體區塊中。要轉換到的緩衝區也是連續、正確交換和對齊的。fromarr 和 toarr 引數應僅用於彈性元素大小的陣列(字串、unicode、void)。

一個 castfunc 範例是

static void
double_to_float(double *from, float* to, npy_intp n,
                void* ignore1, void* ignore2) {
    while (n--) {
          (*to++) = (double) *(from++);
    }
}

然後可以使用以下程式碼註冊它以將雙精度浮點數轉換為單精度浮點數

doub = PyArray_DescrFromType(NPY_DOUBLE);
PyArray_RegisterCastFunc(doub, NPY_FLOAT,
     (PyArray_VectorUnaryFunc *)double_to_float);
Py_DECREF(doub);

註冊強制轉換規則#

預設情況下,所有使用者定義的資料類型都不被認為可以安全地轉換為任何內建資料類型。此外,內建資料類型也不被認為可以安全地轉換為使用者定義的資料類型。這種情況限制了使用者定義的資料類型參與 ufunc 使用的強制轉換系統以及 NumPy 中發生自動強制轉換的其他時間的能力。可以透過將資料類型註冊為可以從特定資料類型物件安全轉換來改變這種情況。函數 PyArray_RegisterCanCast (from_descr, totype_number, scalarkind) 應用於指定資料類型物件 from_descr 可以轉換為類型編號為 totype_number 的資料類型。如果您不嘗試更改純量強制轉換規則,請對 scalarkind 引數使用 NPY_NOSCALAR

如果您希望允許您的新資料類型也能夠參與純量強制轉換規則,那麼您需要在資料類型物件的「.f」成員中指定 scalarkind 函數,以傳回新資料類型應被視為的純量類型(純量的值可供該函數使用)。然後,您可以為可能從使用者定義的資料類型傳回的每個純量類型單獨註冊可以轉換為的資料類型。如果您未註冊純量強制轉換處理,那麼您的所有使用者定義的資料類型都將被視為 NPY_NOSCALAR

註冊 ufunc 迴圈#

您可能還想為您的資料類型註冊低階 ufunc 迴圈,以便可以將數學運算無縫地應用於您的資料類型的 ndarray。註冊具有完全相同 arg_types 簽章的新迴圈會靜默地取代先前為該資料類型註冊的任何迴圈。

在您可以為 ufunc 註冊一維迴圈之前,必須先建立 ufunc。然後,您可以使用迴圈所需的資訊呼叫 PyUFunc_RegisterLoopForType (…)。如果過程成功,則此函數的傳回值為 0;如果過程不成功,則傳回值為 -1,並設定錯誤條件。

在 C 語言中子類化 ndarray#

自 Python 2.2 以來一直潛伏在 Python 中的較少使用的功能之一是在 C 語言中對類型進行子類化的能力。此功能是將 NumPy 基於 Numeric 程式碼庫的重要原因之一,Numeric 程式碼庫已經在 C 語言中。C 語言中的子類型在記憶體管理方面提供了更大的彈性。即使您對如何為 Python 建立新類型只有基本的了解,C 語言中的子類化也不困難。雖然從單一父類型進行子類化最容易,但從多個父類型進行子類化也是可能的。C 語言中的多重繼承通常不如 Python 中的多重繼承有用,因為 Python 子類型的限制是它們具有二進位相容的記憶體佈局。也許由於這個原因,從單一父類型進行子類化稍微容易一些。

與 Python 物件對應的所有 C 結構都必須以 PyObject_HEAD(或 PyObject_VAR_HEAD)開頭。同樣,任何子類型都必須具有以與父類型(或多重繼承情況下的所有父類型)完全相同的記憶體佈局開頭的 C 結構。這樣做的原因是 Python 可能會嘗試存取子類型結構的成員,就好像它具有父結構一樣(即,它會將給定的指標轉換為指向父結構的指標,然後取消引用其成員之一)。如果記憶體佈局不相容,那麼此嘗試將導致不可預測的行為(最終導致記憶體違規和程式崩潰)。

PyObject_HEAD 中的元素之一是指向類型物件結構的指標。透過建立新的類型物件結構並使用函數和指標填充它來描述類型的所需行為,從而建立新的 Python 類型。通常,還會建立新的 C 結構來包含每種類型物件所需的特定於實例的資訊。例如,&PyArray_Type 是指向 ndarray 的類型物件表的指標,而 PyArrayObject* 變數是指向 ndarray 的特定實例的指標(ndarray 結構的成員之一反過來是指向類型物件表 &PyArray_Type 的指標)。最後,必須為每個新的 Python 類型呼叫 PyType_Ready (<pointer_to_type_object>)。

建立子類型#

若要建立子類型,必須遵循類似的程序,只是不同的行為需要在類型物件結構中新增條目。所有其他條目都可以為 NULL,並且將由 PyType_Ready 使用來自父類型的適當函數填充。特別是,若要在 C 語言中建立子類型,請依照下列步驟操作

  1. 如果需要,建立一個新的 C 結構來處理您類型的每個實例。典型的 C 結構將是

    typedef _new_struct {
        PyArrayObject base;
        /* new things here */
    } NewArrayObject;
    

    請注意,完整的 PyArrayObject 用作第一個條目,以確保新類型的實例的二進位佈局與 PyArrayObject 相同。

  2. 使用指向新函數的指標填充新的 Python 類型物件結構,這些指標將覆蓋預設行為,同時將任何應保持不變的函數保留為未填充(或 NULL)。tp_name 元素應有所不同。

  3. 使用指向(主要)父類型物件的指標填充新類型物件結構的 tp_base 成員。對於多重繼承,還可以使用包含所有父物件的元組填充 tp_bases 成員,這些父物件應按順序用於定義繼承。請記住,所有父類型都必須具有相同的 C 結構,多重繼承才能正常運作。

  4. 呼叫 PyType_Ready (<pointer_to_new_type>)。如果此函數傳回負數,則表示發生故障且類型未初始化。否則,類型已準備好使用。通常重要的是將對新類型的參考放入模組字典中,以便可以從 Python 存取它。

有關在 C 語言中建立子類型的更多資訊,可以透過閱讀 PEP 253(可在 https://www.python.org/dev/peps/pep-0253 取得)來學習。

ndarray 子類化的特定功能#

陣列使用一些特殊方法和屬性,以便於子類型與基本 ndarray 類型的互操作。

__array_finalize__ 方法#

ndarray.__array_finalize__#

ndarray 的幾個陣列建立函數允許指定要建立的特定子類型。這允許在許多例程中無縫處理子類型。但是,當以這種方式建立子類型時,既不會呼叫 __new__ 方法,也不會呼叫 __init__ 方法。相反,會分配子類型並填寫適當的實例結構成員。最後,在物件字典中查閱 __array_finalize__ 屬性。如果它存在且不為 None,則它可以是包含指向 PyArray_FinalizeFunc 的指標的 PyCapsule,也可以是採用單一引數的方法(可以是 None)。

如果 __array_finalize__ 屬性是 PyCapsule,則指標必須是指向具有以下簽章的函數的指標

(int) (PyArrayObject *, PyObject *)

第一個引數是新建立的子類型。第二個引數(如果不是 NULL)是「父」陣列(如果陣列是使用切片或在存在明顯可區分的父項的某些其他運算中建立的)。此例程可以執行任何它想執行的操作。它應該在發生錯誤時傳回 -1,否則傳回 0。

如果 __array_finalize__ 屬性不是 None 也不是 PyCapsule,則它必須是一個 Python 方法,該方法將父陣列作為引數(如果沒有父陣列,則可以是 None),並且不傳回任何內容。此方法中的錯誤將被捕獲和處理。

__array_priority__ 屬性#

ndarray.__array_priority__#

此屬性允許簡單但彈性地確定在涉及兩個或多個子類型的運算出現時,應將哪個子類型視為「主要」。在使用不同子類型的運算中,具有最大 __array_priority__ 屬性的子類型將決定輸出的子類型。如果兩個子類型具有相同的 __array_priority__,則第一個引數的子類型決定輸出。預設的 __array_priority__ 屬性為基本 ndarray 類型傳回值 0.0,為子類型傳回值 1.0。此屬性也可以由不是 ndarray 子類型的物件定義,並且可以用於確定應為傳回輸出呼叫哪個 __array_wrap__ 方法。

__array_wrap__ 方法#

ndarray.__array_wrap__#

任何類別或類型都可以定義此方法,該方法應採用 ndarray 引數並傳回類型的實例。它可以被視為 __array__ 方法的相反方法。ufunc(和其他 NumPy 函數)使用此方法來允許其他物件通過。對於 Python >2.4,它也可以用於編寫裝飾器,將僅適用於 ndarray 的函數轉換為適用於具有 __array____array_wrap__ 方法的任何類型的函數。