超越基础#
迭代数组中的元素#
基本迭代#
一个常见的算法要求是能够遍历多维数组中的所有元素.数组迭代器对象使这很容易以通用方式完成,该方法适用于任何维度的数组.当然,如果您知道要使用的维度数,那么您始终可以编写嵌套的 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 ) 很有用,它将跳转到 index 值给出的 1-d 索引. 但是,最常见的用法如下例所示.
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);
}
广播多个数组#
当多个数组参与运算时,您可能希望使用与数学运算(即 ufuncs)相同的广播规则. 这可以使用 PyArrayMultiIterObject 轻松完成. 这是从 Python 命令 numpy.broadcast 返回的对象,并且从 C 中使用它几乎一样容易. 函数 PyArray_MultiIterNew ( n , ... ) 用于(用 n 个输入对象代替 ... ). 输入对象可以是数组或可以转换为数组的任何对象. 返回指向 PyArrayMultiIterObject 的指针. 广播已经完成,它调整迭代器,以便为每个输入调用 PyArray_ITER_NEXT 来前进到每个数组中的下一个元素. 此递增由 PyArray_MultiIter_NEXT ( obj ) 宏自动执行(它可以将多重迭代器 obj 处理为 PyArrayMultiIterObject* 或 PyObject* ). 可以使用 PyArray_MultiIter_DATA ( obj , i ) 获得来自输入编号 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).正在循环使用的代码很可能还需要每个迭代器的 strides 数据.此信息存储在 multi->iters[i]->strides 中.
在 NumPy 源代码中有几个使用多重迭代器的示例,因为它使编写 N 维广播代码非常简单.浏览源代码以获取更多示例.
用户定义的数据类型#
NumPy 带有 24 个内置数据类型.虽然这涵盖了绝大多数可能的用例,但可以想象用户可能需要额外的数据类型.NumPy 系统提供了一些支持来添加额外的数据类型.这种额外的数据类型与常规数据类型的行为非常相似,除了 ufunc 必须注册 1-d 循环以单独处理它.此外,检查其他数据类型是否可以"安全地"转换为这种新类型,或者从这种新类型转换,始终返回"可以转换",除非您还注册了您的新数据类型可以转换为和从转换的类型.
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 本质上是线程不安全的.有关 NumPy 中线程安全的更多详细信息,请参见 thread_safety .
注册强制转换函数#
您可能希望允许内置的(以及其他用户定义的)数据类型自动转换为您的数据类型.为了使这成为可能,您必须使用您想要能够转换的数据类型注册一个强制转换函数.这需要为每个您想要支持的转换编写低级强制转换函数,然后使用数据类型描述符注册这些函数.低级强制转换函数具有以下签名.
-
void castfunc(void *from, void *to, npy_intp n, void *fromarr, void *toarr)#
将
n个元素从一种类型from转换为另一种类型to.要转换的数据位于由 from 指向的连续,正确交换和对齐的内存块中.要转换为的缓冲区也是连续的,正确交换和对齐的.fromarr 和 toarr 参数只应用于灵活元素大小的数组(字符串,unicode,void).
一个例子是:]]</item> <item id=”1432”><![CDATA[然后可以使用以下代码将其注册为将双精度浮点数转换为浮点数:]]</item> <item id=”1433”><![CDATA[注册强制转换规则
static void
double_to_float(double *from, float* to, npy_intp n,
void* ignore1, void* ignore2) {
while (n--) {
(*to++) = (double) *(from++);
}
}
This could then be registered to convert doubles to floats using the code:
doub = PyArray_DescrFromType(NPY_DOUBLE);
PyArray_RegisterCastFunc(doub, NPY_FLOAT,
(PyArray_VectorUnaryFunc *)double_to_float);
Py_DECREF(doub);
Registering coercion rules#
默认情况下,所有用户定义的数据类型都不会被假定为可以安全地转换为任何内置数据类型.此外,内置数据类型也不会被假定为可以安全地转换为用户定义的数据类型.这种情况限制了用户定义的数据类型参与 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注册一个1-d循环之前,必须先创建该ufunc.然后,您使用循环所需的信息调用 PyUFunc_RegisterLoopForType (…).如果过程成功,则此函数的返回值为 0 ,如果未成功,则返回 -1 并设置错误条件.
在C中对ndarray进行子类型化#
自 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 的类型对象表的指针,而 PyArrayObject* 变量是指向 ndarray 的特定实例的指针(ndarray 结构的一个成员反过来是指向类型对象表 &PyArray_Type 的指针).最后,必须为每个新的 Python 类型调用 PyType_Ready (<pointer_to_type_object>).
创建子类型#
要创建子类型,必须遵循类似的过程,只是只有不同的行为才需要在类型对象结构中创建新条目.所有其他条目都可以为 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 访问它.
有关在 C 中创建子类型的更多信息,可以通过阅读 PEP 253(可在 https://www.python.org/dev/peps/pep-0253 获取)来了解.
ndarray 子类型的特定功能#
数组使用一些特殊的方法和属性,以便于子类型与基本 ndarray 类型的互操作.
__array_finalize__ 方法#
- ndarray.__array_finalize__#
ndarray 的几个数组创建函数允许指定要创建的特定子类型.这允许在许多例程中无缝处理子类型.但是,当以这种方式创建子类型时,既不会调用 __new__ 方法,也不会调用 __init__ 方法.相反,会分配子类型并填充相应的实例结构成员.最后,在对象字典中查找
__array_finalize__属性.如果它存在且不为 None,那么它可以是包含指向PyCapsule的指针的PyArray_FinalizeFunc,也可以是接受单个参数的方法(可以是 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__方法的反面.此方法被 ufuncs (以及其他 NumPy 函数)用来允许其他对象通过.对于 Python >2.4,它也可以用来编写一个装饰器,用于将仅适用于 ndarrays 的函数转换为适用于任何具有__array__和__array_wrap__方法的类型的函数.