深入的知識

迭代數組中的元素

基本迭代

一種常見的算法要求是能夠遍歷多維數組中的所有元素。數組迭代器對象使這種方法易于以通用方式完成,適用于任何維度的數組。當然,如果您知道要使用的維數,那么您始終可以編寫嵌套for循環來完成迭代。但是,如果要編寫適用于任意數量維度的代碼,則可以使用數組迭代器。訪問數組的.flat屬性時返回數組迭代器對象。

基本用法是調用PyArray_IterNew

array),其中array是ndarray對象(或其子類之一)。返回的對象是一個array-iterator對象(由ndarray的.flat屬性返回的同一對象)。此對象通常強制轉換為PyArrayIterObject *,以便可以訪問其成員。所需的唯一成員iter->size包含數組的總大小iter->index,其中包含數組的當前1-d索引,以及iter->dataptr指向數組當前元素的數據的指針。有時,訪問iter->ao哪個是指向底層ndarray對象的指針也很有用。

在數組的當前元素處理數據之后,可以使用macro PyArray_ITER_NEXT

iter)獲取數組的下一個元素
。迭代總是以C風格的連續方式進行(最后一個索引變化最快)。的
PyArray_ITER_GOTO iter,destination)可以用來跳到一個特定點的數組,其中在destination是npy_intp數據類型與空間的數組,以處理潛在的數組中的維度中的至少數。有時使用PyArray_ITER_GOTO1D

iter,index)將跳轉到由值給出的1-d索引是有用的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)構造被修改,使得它不會在由暗淡指示的尺寸迭代的迭代器對象。這個迭代器對象的唯一限制是不能使用PyArray_Iter_GOTO1Dit,ind)宏(因此,如果將此對象傳遞回Python,則平面索引將不起作用 - 所以你不應該這樣做)。請注意,此例程中返回的對象仍然通常轉換為PyArrayIterObject *。所做的就是修改返回迭代器的步幅和尺寸,以模擬迭代數組[...,0,...],其中0放在
維度上。如果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);
}

在多個數組上廣播

當一個操作涉及多個數組時,您可能希望使用數學操作(即ufuncs)使用的相同廣播規則。
這可以使用 PyArrayMultiIterObject

輕松完成。
這是從Python命令numpy.Broadcast返回的對象,它幾乎和C一樣容易使用。
函數 PyArray_MultiIterNew (n, ...)。使用(n個輸入對象代替)。
輸入對象可以是數組或任何可以轉換為數組的對象。
返回指向 PyArrayMultiIterObject 的指針。
廣播已經完成,它調整迭代器,以便為每個輸入調用PyArray_ITER_NEXT,
以便前進到每個數組中的下一個元素。
這種遞增由 PyArray_MultiIter_NEXT (Obj)宏自動執行(它可以將乘法器 obj 處理為 PyArrayMultiObject * 或 PyObject * )。
輸入編號 i 中的數據可使用PyArray_MultiIter_DATA

(obj, i)和總(廣播)大小作為PyArray_MultiIter_SIZE(Obj)。
下面是使用此功能的示例。

mobj = PyArray_MultiIterNew(2, obj1, obj2);
size = PyArray_MultiIter_SIZE(obj);
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);
}

function PyArray_RemoveSmallest

(multi) 可用于獲取多迭代器對象并調整所有迭代器,以便迭代不會發生在最大維度上(它使得該維度的大小為1)。
循環使用指針的代碼很可能也需要每個迭代器的步幅數據。此信息存儲在 multi->iters[i]->strides 中。

在NumPy源代碼中使用多迭代器有幾個例子,因為它使N維廣播代碼編寫起來非常簡單。瀏覽源代碼以獲取更多示例。

用戶定義的數據類型

NumPy帶有24種內置數據類型。雖然這涵蓋了絕大多數可能的用例,但可以想象用戶可能需要額外的數據類型。有一些支持在NumPy系統中添加額外的數據類型。此附加數據類型的行為與常規數據類型非常相似,只是ufunc必須具有1-d循環才能單獨處理它。同時檢查其他數據類型是否可以“安全”地轉換到這種新類型或從這種新類型轉換為“can cast”,除非您還注冊了新數據類型可以轉換為哪種類型。添加數據類型是NumPy 1.0中經過較少測試的領域之一,因此該方法可能存在漏洞。如果使用已有的OBJECT或VOID數據類型無法執行您想要執行的操作,則僅添加新數據類型。

添加新數據類型

要開始使用新的數據類型,您需要首先定義一個新的Python類型來保存新數據類型的標量。如果您的新類型具有二進制兼容布局,則可以接受從其中一個數組標量繼承。這將允許您的新數據類型具有數組標量的方法和屬性。新數據類型必須具有固定的內存大?。ㄈ绻x需要靈活表示的數據類型,如變量精度數,則使用指向對象的指針作為數據類型)。新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。此類型編號應存儲并由您的模塊提供,以便其他模塊可以使用它來識別您的數據類型(查找用戶定義的數據類型編號的另一種機制是根據類型的名稱進行搜索 - 與數據類型相關聯的對象PyArray_TypeNumFromName)。

注冊投射功能






您可能希望允許內置(和其他用戶定義的)數據類型自動轉換為您的數據類型。為了實現這一點,您必須使用您希望能夠從中投射的數據類型注冊一個轉換函數。這需要為要支持的每個轉換編寫低級轉換函數,然后使用數據類型描述符注冊這些函數。低級轉換函數具有簽名。

void castfunc( void * from ,void * to ,npy_intp

n ,void * fromarr ,void * toarr?

n元件from一個鍵入to另一個。要轉換的數據位于由from指向的連續,正確交換和對齊的內存塊中。要轉換為的緩沖區也是連續的,正確交換和對齊的。fromarr和toarr參數只應用于靈活元素大小的數組(字符串,unicode,void)。

一個示例castfunc是:

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

然后可以使用以下代碼注冊以將雙精度轉換為浮點數:

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

注冊強制規則

默認情況下,不會假定所有用戶定義的數據類型都可安全地轉換為任何內置數據類型。此外,不假定內置數據類型可安全地轉換為用戶定義的數據類型。這種情況限制了用戶定義的數據類型參與ufuncs使用的強制系統的能力,以及在NumPy中進行自動強制時的其他情況。這可以通過將數據類型注冊為從特定數據類型對象安全地轉換來更改。函數PyArray_RegisterCanCast

(from_descr,totype_number,scalarkind)應該用于指定數據類型對象from_descr可以轉換為類型號為totype_number的數據類型。如果您不想改變標量強制規則,那么請使用NPY_NOSCALARscalarkind參數。

如果要允許新數據類型也能夠共享標量強制規則,則需要在數據類型對象的“.f”成員中指定scalarkind函數,以返回新數據的標量類型-type應該被視為(標量的值可用于該函數)。然后,您可以為可以從用戶定義的數據類型返回的每個標量類型注冊可以單獨轉換的數據類型。如果您沒有注冊標量強制處理,那么所有用戶定義的數據類型都將被視為NPY_NOSCALAR。

注冊ufunc循環

您可能還希望為數據類型注冊低級ufunc循環,以便數據類型的ndarray可以無縫地應用數學。注冊具有完全相同的arg_types簽名的新循環,靜默替換該數據類型的任何先前注冊的循環。

在為ufunc注冊一維循環之前,必須預先創建ufunc。
然后調用 PyUFunc_RegisterLoopForType

(…)。
以及循環所需的信息。
如果進程成功,則此函數的返回值為0;
如果進程不成功,則返回 -1,并設置錯誤條件。

在C中對ndarray進行子類型化

自2.2以來一直潛伏在Python中的一個較少使用的功能是在C中子類類型的能力。這個設施是使NumPy脫離已經在C中的數字代碼庫的重要原因之一。 C中的子類型允許在內存管理方面具有更大的靈活性。即使您對如何為Python創建新類型有基本的了解,在C中進行子類型輸入并不困難。雖然最簡單的是從單個父類型進行子類型化,但也可以從多個父類型進行子類型化。C中的多重繼承通常沒有Python中那么有用,因為對Python子類型的限制是它們具有二進制兼容的內存布局。也許由于這個原因,從單個父類型子類型更容易一些。

與Python對象相對應的所有C結構必須以PyObject_HEAD

(或PyObject_VAR_HEAD

)開頭
。同樣,任何子類型都必須具有C結構,該結構以與父類型完全相同的內存布局(或多重繼承的情況下的所有父類型)開始。這樣做的原因是Python可能會嘗試訪問子類型結構的成員,就像它具有父結構一樣( 它會將指定的指針強制轉換為指向父結構的指針,然后取消引用其中一個成員)。如果內存布局不兼容,則此嘗試將導致不可預測的行為(最終導致內存沖突和程序崩潰)。

PyObject_HEAD

中的元素之一是指向 type-object 結構的指針。
通過創建一個新的類型-對象結構并用函數和指針填充它來創建一個新的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類型對象結構,這些新函數將覆蓋默認行為,同時保留任何應該保持相同的未填充(或空)的函數。tp_name元素應該不同。

  3. 用指向(Main)父類型對象的指針填充新類型對象結構的tp_base成員。對于多重繼承,還要用一個元組填充tp_base成員,該元組包含所有父對象(按照它們用于定義繼承的順序)。請記住,所有父類型必須具有相同的C結構,才能使多重繼承正常工作。

  4. 調用PyType_Ready

  1. (<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,那么它可以是包含指向a的指針的CObject,PyArray_FinalizeFunc也可以是采用單個參數的方法(可以是None)。

如果__array_finalize__

屬性是CObject,
則指針必須是指向具有簽名的函數的指針:

(int) (PyArrayObject *, PyObject *)

第一個參數是新創建的子類型。
第二個參數(如果不是NULL)是“父”數組(如果數組是使用切片或其他操作創建的,其中存在明顯可區分的父項)。
這個例程可以做任何想做的事情。它應該在錯誤時返回-1,否則返回0。

如果__array_finalize__

  • 屬性不是None也不是CObject,
    那么它必須是一個Python方法,它將父數組作為參數(如果沒有父元素,則可以是None),
    并且不返回任何內容。將捕獲并處理此方法中的錯誤。

__array_priority__屬性

  • ndarray.__array_priority__

    當涉及兩個或更多個子類型的操作出現時,該屬性允許簡單但靈活地確定哪個子類型應被視為“主要”。在使用不同子類型的操作中,具有最大__array_priority__


屬性的子類型將確定輸出的子類型。如果兩個子類型相同,__array_priority__ 則第一個參數的子類型確定輸出。__array_priority__ 對于基本ndarray類型,default
屬性返回值0.0,對于子類型,返回1.0。此屬性也可以由不是ndarray的子??類型的對象定義,并且可以用于確定__array_wrap__
  • 應該為返回輸出調用哪個方法。

__array_wrap__方法

  • ndarray.__array_wrap__

    任何類或類型都可以定義此方法,該方法應采用ndarray參數并返回該類型的實例。
    它可以看作是該__array__

方法的反面。
ufuncs(和其他NumPy函數)使用此方法允許其他對象通過。
對于Python > 2.4,它也可以用來編寫一個裝飾器,
它將一個僅適用于ndarrays的函數轉換為一個可以使用__array__ __array_wrap__
  • 方法處理任何類型的函數。

作者:柯廣的網絡日志 ? 深入的知識
微信公眾號:Java大數據與數據倉庫