NumPy用户指南 >使用NumPy C-API > 超越基础
一种常见的算法要求是能够遍历多维数组中的所有元素。数组迭代器对象使它易于以适用于任何维数数组的通用方式来完成。自然地,如果您知道将要使用的维数,则始终可以编写嵌套的for循环来完成迭代。但是,如果您想编写适用于任何尺寸的代码,则可以使用数组迭代器。访问数组的.flat属性时,将返回数组迭代器对象。
基本用法是调用PyArray_IterNew
(array
),其中array是ndarray对象(或其子类之一)。返回的对象是一个数组迭代器对象(由ndarray的.flat属性返回的对象相同)。通常将此对象强制转换为PyArrayIterObject *,以便可以访问其成员。唯一需要的成员是iter->size
包含数组的总大小iter->index
,包含数组的当前1-d索引以及iter->dataptr
指向数组当前元素的数据的指针。有时,访问iter->ao
指向基础ndarray对象的指针也很有用。
在数组的当前元素处处理完数据后,可以使用宏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
)重置一个iterator对象返回数组的开头。
在这一点上应该强调的是,如果您的数组已经是连续的,则可能不需要数组迭代器(使用数组迭代器可以,但是比您可以编写的最快的代码要慢)。数组迭代器的主要目的是用任意步长封装N维数组上的迭代。在NumPy源代码本身的许多很多地方都使用了它们。如果您已经知道数组是连续的(Fortran或C),则只需将element-size添加到正在运行的指针变量中,将非常有效地引导您遍历数组。换句话说,在连续情况下(假设为双精度),这样的代码对于您来说可能会更快。
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_GOTO1D
(it
,ind
)宏不能使用(如果你通过这个对象在Python这样扁平索引不会工作-所以你不应该这样做)。请注意,从该例程返回的对象通常仍转换为PyArrayIterObject *。完成的所有操作都是修改返回的迭代器的步幅和尺寸,以模拟对array […,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
)宏自动执行的
(该宏可以将multiterator处理obj
为a 或a
)。输入数字中的数据可以使用
(,),总(广播的)大小为()。以下是使用此功能的示例。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);
}
函数PyArray_RemoveSmallest
(multi
)可用于获取多迭代器对象并调整所有迭代器,以使迭代不会在最大维度上发生(它使该维度的大小为1)。使用指针的循环代码很可能还需要每个迭代器的步幅数据。此信息存储在multi-> iters [i]->步幅中。
在NumPy源代码中使用多重迭代器有几个示例,因为它使N维广播代码的编写非常简单。浏览源代码以获取更多示例。
NumPy带有24种内置数据类型。尽管这涵盖了大多数可能的用例,但可以想象,用户可能需要其他数据类型。对于将其他数据类型添加到NumPy系统中,存在一些支持。这种额外的数据类型的行为与常规数据类型非常相似,只是ufunc必须注册一维循环才能单独处理它。还要检查是否可以将其他数据类型“安全地”强制转换到该新类型或从该新类型强制转换为其他类型,除非您还注册了可以将新数据类型强制从该类型强制转换的类型,否则将始终返回“可以强制转换”。
NumPy源代码在其测试套件中包括一个自定义数据类型的示例。_rational_tests.c.src
源代码目录 numpy/numpy/core/src/umath/
中的文件包含一个数据类型的实现,该数据类型表示有理数为两个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”成员中定义所需的函数:非零,copyswap,copyswapn,setitem,getitem和cast。您定义的“ .f”成员中的功能越多,新数据类型将越有用。将未使用的函数初始化为NULL非常重要。这可以使用PyArray_InitArrFuncs
(f)来实现。
一旦PyArray_Descr
创建了一个新结构,并填充了所需的信息和有用的函数
PyArray_RegisterDataType
(new_descr)。此调用的返回值是一个整数,为您提供一个唯一的type_number,用于指定您的数据类型。此类型编号应由您的模块存储并可以使用,以便其他模块可以使用它识别您的数据类型(查找用户定义的数据类型编号的另一种机制是根据类型名称进行搜索,与与数据类型相关联的对象PyArray_TypeNumFromName
。
您可能希望允许将内置(和其他用户定义的)数据类型自动转换为您的数据类型。为了使之成为可能,您必须使用您希望从中进行转换的数据类型注册一个转换函数。这要求为您要支持的每个转换编写低级转换函数,然后将这些函数注册到数据类型描述符中。低级转换功能具有签名。
castfunc
( void * from,void * 至,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* 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的数据类型。如果您不尝试更改标量强制规则,则将其NPY_NOSCALAR
用于scalarkind参数。
如果要允许新数据类型也可以在标量强制规则中共享,则需要在数据类型对象的“ .f”成员中指定scalarkind函数,以返回新数据标量的类型-type应该被视为(该函数可以使用标量的值)。然后,可以注册可以从用户定义的数据类型返回的每种标量类型分别转换为的数据类型。如果您未注册标量强制处理,则所有用户定义的数据类型都将显示为NPY_NOSCALAR
。
您可能还需要为数据类型注册低级ufunc循环,以便可以将数据类型的ndarray无缝应用数学。注册具有完全相同的arg_types签名的新循环,将以无提示方式替换该数据类型的所有先前注册的循环。
在为ufunc注册1-d循环之前,必须先创建ufunc。然后,调用PyUFunc_RegisterLoopForType
(…)并提供循环所需的信息。该函数的返回值是0
该过程是否成功,-1
如果失败则设置错误条件。
自2.2以来一直在Python中潜伏的较少使用的功能之一是能够在C中对类型进行子类化。此功能是使NumPy脱离已经在C中的Numeric代码库的重要原因之一。 C中的子类型在内存管理方面提供了更大的灵活性。即使您仅对如何为Python创建新类型有基本的了解,在C中进行子类型也不难。虽然最容易从单个父类型进行子类型化,但也可以从多个父类型进行子类型化。C中的多重继承通常不如Python有用,因为对Python子类型的限制是它们具有二进制兼容的内存布局。也许由于这个原因,从单个父类型进行子类型比较容易。
对应于Python对象的所有C结构都必须以PyObject_HEAD
(或PyObject_VAR_HEAD
)开头
。以相同的方式,任何子类型都必须具有一个C结构,该结构的开始于与父类型(或在多重继承的情况下为所有父类型)完全相同的内存布局。这样做的原因是Python可能会尝试访问子类型结构的成员,就好像它具有父结构一样(即,它将给定的指针转换为指向父结构的指针,然后取消引用其成员之一)。如果内存布局不兼容,则此尝试将导致无法预料的行为(最终导致内存冲突和程序崩溃)。
其中的元素之一PyObject_HEAD
是指向类型对象结构的指针。通过创建新的类型对象结构并用函数和指针填充该结构来创建所需的行为,从而创建新的Python类型。通常,还会创建一个新的C结构,以包含该类型的每个对象所需的实例特定信息。例如,
&PyArray_Type
是指向ndarray的类型对象表的指针,而变量是指向ndarray的特定实例的指针(ndarray结构的成员之一又是指向类型对象表的指针)。最后,
必须为每种新的Python类型调用(<pointer_to_type_object>)。PyArrayObject *
&PyArray_Type
PyType_Ready
要创建子类型,必须遵循类似的过程,只是只有不同的行为才需要在类型对象结构中添加新的条目。所有其他条目都可以为NULL,并由PyType_Ready
来自父类型的适当函数填充。特别是,要在C中创建子类型,请按照以下步骤操作:
如果需要,创建一个新的C结构来处理您类型的每个实例。典型的C结构为:
typedef _new_struct {
PyArrayObject base;
/* new things here */
} NewArrayObject;
请注意,完整的PyArrayObject用作第一个条目,以确保新类型的实例的二进制布局与PyArrayObject相同。
用指向新函数的指针填充新的Python类型对象结构,这些指针将覆盖默认行为,同时保留所有应保持不变的函数(或NULL)。tp_name元素应该不同。
用指向(主)父类型对象的指针填充新的类型对象结构的tp_base成员。对于多继承,还应在tp_bases成员中填充一个元组,该元组包含应用于定义继承的所有父对象的顺序。请记住,所有父类型必须具有相同的C结构,多重继承才能正常工作。
调用PyType_Ready
(<pointer_to_new_type>)。如果此函数返回负数,则发生故障并且类型未初始化。否则,该类型可以使用了。通常,将对新类型的引用放置在模块字典中很重要,以便可以从Python访问它。
通过阅读PEP 253(可从https://www.python.org/dev/peps/pep-0253获得),可以了解有关在C中创建子类型的更多信息。
数组使用一些特殊的方法和属性,以促进子类型与基本ndarray类型的互操作。
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),并且不返回任何内容。此方法中的错误将被捕获并处理。
ndarray.
__array_priority__
¶当涉及两个或多个子类型的操作出现时,此属性允许简单而灵活地确定应将哪个子类型视为“主要”。在使用不同子类型的操作中,具有最大__array_priority__
属性的子类型将确定输出的子类型。如果两个子类型相同,__array_priority__
则第一个参数的子类型确定输出。默认
__array_priority__
属性对于基本ndarray类型返回值0.0,对于子类型返回1.0。该属性还可以由非ndarray子类型的对象定义,并可以用于确定__array_wrap__
应为返回输出调用哪种方法。
ndarray.
__array_wrap__
¶任何类或类型都可以定义此方法,该方法应使用ndarray参数并返回该类型的实例。可以将其视为与__array__
方法相反的方法。ufunc(和其他NumPy函数)使用此方法来允许其他对象通过。对于Python> 2.4,它也可用于写一个装饰,其将只有具有ndarrays工作以一个与任何类型的可与一个功能__array__
和__array_wrap__
方法。