超越基础#

发现之旅不在于寻找新的风景,而在于拥有新的眼光.
新视角.
马塞尔·普鲁斯特
发现是看到每个人都能看到的东西,并思考没有人想过的事情.
没有人想过.
Albert Szent-Gyorgi

迭代数组中的元素#

基本迭代#

一个常见的算法需求是能够遍历多维数组中的所有元素.数组迭代器对象使得以通用方式完成这项工作变得容易,这种方式适用于任何维度的数组.当然,如果你知道将要使用的维度数量,那么你总是可以编写嵌套的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*).输入编号 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 系统对添加额外的数据类型有一些支持.这个额外的数据类型将表现得非常像一个常规的数据类型,除了 ufuncs 必须注册 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 指向的连续、正确交换和对齐的内存块中.要转换到的缓冲区也是连续、正确交换和对齐的.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);

注册强制规则#

默认情况下,所有用户定义的数据类型都不被认为是安全地可转换为任何内置数据类型的.此外,内置数据类型也不被认为是安全地可转换为用户定义的数据类型的.这种情况限制了用户定义的数据类型参与由ufuncs和其他自动转换发生时使用的强制系统的能力.这可以通过注册数据类型为从特定数据类型对象安全地可转换来改变.函数 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 进行子类型化#

自2.2版本以来,Python中一个较少使用的功能是能够在C中子类化类型.这一功能是基于已经用C编写的Numeric代码库构建NumPy的重要原因之一.C中的子类型在内存管理方面提供了更大的灵活性.即使你只有创建新Python类型的基本理解,在C中进行子类型化也不难.虽然从单一父类型进行子类型化最为简单,但从多个父类型进行子类型化也是可能的.在C中,多重继承通常不如在Python中那么有用,因为对Python子类型的一个限制是它们具有二进制兼容的内存布局.也许因为这个原因,从单一父类型进行子类型化稍微容易一些.

所有对应于Python对象的C结构体必须以 PyObject_HEAD (或 PyObject_VAR_HEAD)开始.同样地,任何子类型必须有一个C结构体,该结构体以与父类型(或多个继承情况下的所有父类型)完全相同的内存布局开始.这样做的理由是,Python可能会尝试将子类型结构的成员当作父类型结构来访问(即,它会将给定的指针转换为指向父类型结构的指针,然后解引用其成员之一).如果内存布局不兼容,那么这种尝试将导致不可预测的行为(最终导致内存违规和程序崩溃).

PyObject_HEAD 中的一个元素是指向类型对象结构的指针.通过创建一个新的类型对象结构并填充函数和指针来描述该类型的期望行为,从而创建一个新的 Python 类型.通常,还会创建一个新的 C 结构来包含该类型每个对象所需的实例特定信息.例如,:c:data:&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__方法.相反,子类型被分配,并且适当的实例结构成员被填充.最后,在对象字典中查找:obj:如果它存在且不为None,那么它可以是一个包含指向:c:func:`PyArray_FinalizeFunc`指针的:c:type:`PyCapsule,或者它可以是一个接受单个参数(可以是None)的方法

如果 __array_finalize__ 属性是一个 PyCapsule,那么指针必须是一个指向具有以下签名的函数的指针:

(int) (PyArrayObject *, PyObject *)

第一个参数是新创建的子类型.第二个参数(如果不是NULL)是”父”数组(如果数组是通过切片或其他操作创建的,其中存在一个明显的可区分的父数组).这个例程可以做任何它想做的事情.它应该在错误时返回-1,否则返回0.

如果 __array_finalize__ 属性不是 None 也不是 PyCapsule,那么它必须是一个 Python 方法,该方法接受父数组作为参数(如果没有父数组,则可能是 None),并且不返回任何内容.此方法中的错误将被捕获并处理.

__array_priority__ 属性#

ndarray.__array_priority__#

此属性允许在涉及两种或更多子类型的操作中简单但灵活地确定哪种子类型应被视为”主要”.在使用不同子类型的操作中,具有最大 __array_priority__ 属性的子类型将决定输出(s)的子类型.如果两种子类型具有相同的 __array_priority__,则由第一个参数的子类型决定输出.基 ndarray 类型的默认 __array_priority__ 属性返回值为 0.0,子类型的返回值为 1.0.此属性也可以由不是 ndarray 子类型的对象定义,并可用于确定应调用哪个 __array_wrap__ 方法作为返回输出.

__array_wrap__ 方法#

ndarray.__array_wrap__#

任何类或类型都可以定义这个方法,该方法应接受一个 ndarray 参数并返回该类型的一个实例.它可以看作是 __array__ 方法的反面.这个方法被 ufuncs(和其他 NumPy 函数)使用,以允许其他对象通过.对于 Python >2.4,它还可以用于编写一个装饰器,将仅适用于 ndarrays 的函数转换为适用于具有 __array____array_wrap__ 方法的任何类型的函数.