超越基础

探索之旅不是寻找新的风景,而是拥有
新眼睛。
--- 马塞尔·普鲁斯特
“发现”是看到别人看到的,而想不到。
还有一个想法。
--- Albert Szent-Gyorgi

迭代数组中的元素

基本迭代

一个常见的算法要求是能够遍历多维数组中的所有元素。数组迭代器对象以一种通用的方式很容易做到这一点,该方法适用于任何维度的数组。当然,如果您知道将要使用的维度的数量,那么您总是可以编写嵌套for循环来完成迭代。但是,如果您希望编写可用于任意数量维度的代码,那么可以使用数组迭代器。访问数组的.flat属性时返回数组迭代器对象。

基本用法是调用 PyArray_IterNewarray )其中,array是一个ndarray对象(或其子类之一)。返回的对象是数组迭代器对象(与ndarray的.flat属性返回的对象相同)。此对象通常强制转换为PyarrayInterObject*以便可以访问其成员。唯一需要的成员是 iter->size 它包含数组的总大小, iter->index ,其中包含数组中的当前一维索引,以及 iter->dataptr 它是指向数组当前元素的数据的指针。有时访问也很有用 iter->ao 它是指向基础ndarray对象的指针。

在处理数组当前元素的数据后,可以使用宏获取数组的下一个元素。 PyArray_ITER_NEXTiter )迭代总是以C样式连续的方式进行(最后一个索引变化最快)。这个 PyArray_ITER_GOTOiterdestination )可用于跳转到数组中的特定点,其中 destination 是一个npy-intp数据类型的数组,其中至少有空间处理基础数组中的维度数。偶尔使用它是有用的 PyArray_ITER_GOTO1Diterindex )它将跳转到由以下值给出的一维索引: 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_Checkobj )确保您有一个迭代器对象和 PyArray_ITER_RESETiter )将迭代器对象重置回数组的开头。

此时应该强调的是,如果数组已经是连续的,那么您可能不需要数组迭代器(使用数组迭代器可以工作,但比您可以编写的最快的代码慢)。数组迭代器的主要目的是用任意步幅封装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++;
}

迭代除一个轴以外的所有轴

一种常见的算法是循环数组的所有元素,并通过发出函数调用对每个元素执行某些函数。由于函数调用可能非常耗时,因此加速这种算法的一种方法是编写函数,使其获取数据向量,然后编写迭代,以便每次对整个数据维度执行函数调用。这增加了每个函数调用所完成的工作量,从而将函数调用减少到总时间的一小部分(er)。即使循环的内部是在没有函数调用的情况下执行的,在具有最多元素的维度上执行内部循环也是有利的,以利用微处理器上可用的速度增强功能,这些微处理器使用流水线来增强基本操作。

这个 PyArray_IterAllButAxisarray&dim )构造一个经过修改的迭代器对象,以便它不会在dim指示的维度上迭代。对迭代器对象的唯一限制是 PyArray_ITER_GOTO1Ditind )不能使用宏(因此,如果将此对象传递回python,则平面索引也不起作用---因此不应该这样做)。请注意,此例程返回的对象通常仍被强制转换为PyarrayInterObject。 * . 所做的只是修改返回迭代器的步幅和维度,以模拟对数组的迭代。 […,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);
}

在多个阵列上广播

当一个操作涉及多个数组时,您可能希望使用与数学操作相同的广播规则( i.e. 不明飞行物)的使用。使用 PyArrayMultiIterObject . 这是从python命令numpy.broadcast返回的对象,几乎和从c返回的对象一样容易使用。 PyArray_MultiIterNewn, ... (用于) n input objects in place of ... )输入对象可以是数组或任何可以转换为数组的对象。返回指向PyrayMultiiteRobject的指针。广播已经完成,它调整迭代器,以便所有需要做的事情,以前进到每个数组中的下一个元素是Pyarray-Iter_下一个被调用为每个输入。此递增由自动执行 PyArray_MultiIter_NEXTobj )宏(可以处理多处理器 obj 作为一个 PyArrayMultiObject* 或A PyObject* )输入号码的数据 i 可以使用 PyArray_MultiIter_DATAobji )总(广播)大小为 PyArray_MultiIter_SIZEobj )下面是使用此功能的示例。

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_RemoveSmallestmulti )可用于获取一个多迭代器对象,并调整所有迭代器,使迭代不会发生在最大维度上(它使该维度的大小为1)。循环使用指针的代码很可能也需要每个迭代器的steps数据。此信息存储在multi->iters中 [i] -跨步。

在numpy源代码中有几个使用多迭代器的例子,因为它使得N维广播代码的编写非常简单。浏览源代码以获取更多示例。

用户定义的数据类型

NumPy提供24种内置数据类型。虽然这涵盖了大多数可能的用例,但是可以想象,用户可能需要额外的数据类型。在系统中添加一个额外的数据类型是NumPy。这个额外的数据类型的行为与常规数据类型非常相似,只是ufuncs必须注册1-d循环才能单独处理它。另外,检查其他数据类型是否可以“安全地”转换到此新类型或是否可以从中转换将始终返回“can cast”,除非您还注册了新数据类型可以转换到哪些类型或从哪些类型转换。

NumPy源代码包含一个自定义数据类型的示例,作为其测试套件的一部分。文件 _rational_tests.c.src 在源代码目录中 numpy/numpy/core/src/umath/ 包含将有理数表示为两个32位整数之比的数据类型的实现。

添加新数据类型

要开始使用新的数据类型,首先需要定义一个新的python类型来保存新数据类型的标量。如果新类型具有二进制兼容的布局,则可以从数组标量之一继承。这将允许新的数据类型具有数组标量的方法和属性。新数据类型必须具有固定的内存大小(如果要定义需要灵活表示的数据类型,如可变精度数字,则使用指向对象的指针作为数据类型)。新python类型的对象结构的内存布局必须是pyobject头,后跟数据类型所需的固定大小内存。例如,新的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”成员中的函数越多,新数据类型就越有用。将未使用的函数初始化为空非常重要。这可以通过使用 PyArray_InitArrFuncs (f)。

曾经是新的 PyArray_Descr 结构被创建并填充所需的信息和调用的有用函数 PyArray_RegisterDataType (NeNexDISCR)。此调用的返回值是一个整数,为您提供指定数据类型的唯一类型编号。此类型号应由模块存储和提供,以便其他模块可以使用它来识别您的数据类型(查找用户定义的数据类型号的另一种机制是根据与数据类型关联的类型对象的名称进行搜索,使用 PyArray_TypeNumFromName

注册强制转换函数

您可能希望允许将内置(和其他用户定义的)数据类型自动转换为您的数据类型。为了实现这一点,您必须使用希望能够从中强制转换的数据类型注册强制转换函数。这需要为您想要支持的每个转换编写低级强制转换函数,然后用数据类型描述符注册这些函数。低级的强制转换函数具有签名。

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

铸件 n 元素 from 一种 to 另一个。要从中强制转换的数据位于一个连续的、正确交换的、对齐的内存块中,该内存块由From指向。要转换到的缓冲区也是连续的、正确交换和对齐的。fromarr和toarr参数只能用于灵活的元素大小的数组(string、unicode、void)。

Castfunc的一个例子是:

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

然后可以注册它,使用代码将double转换为float:

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

正在注册强制规则

默认情况下,不假定所有用户定义的数据类型都可以安全地强制转换为任何内置数据类型。此外,内置数据类型不被假定为可以安全地强制转换为用户定义的数据类型。这种情况限制了用户定义的数据类型参与UFuncs使用的强制系统的能力,以及在numpy中发生自动强制时的其他时间。这可以通过将数据类型注册为可从特定数据类型对象安全地强制转换来更改。函数 PyArray_RegisterCanCast (from_descr,to type_number,scalarkind)用于指定可以将来自_descr的数据类型对象强制转换为类型号为to type_number的数据类型。如果不尝试更改标量强制规则,则使用 NPY_NOSCALAR 对于scalarkind参数。

如果希望新数据类型也能够共享标量强制规则,则需要在数据类型对象的“.f”成员中指定scalar kind函数,以返回新数据类型应视为的标量类型(标量的值对该函数可用)。然后,可以为用户定义的数据类型返回的每个标量类型分别注册可强制转换为的数据类型。如果不注册标量强制处理,那么所有用户定义的数据类型都将被视为 NPY_NOSCALAR .

注册ufunc循环

您可能还希望为数据类型注册低级UFUNC循环,以便数据类型的一个ndarray可以无缝地应用数学。用完全相同的arg_类型签名注册新循环,将自动替换该数据类型以前注册的任何循环。

在为ufunc注册一维循环之前,必须先创建ufunc。然后你打电话 PyUFunc_RegisterLoopForType (…)包含循环所需的信息。此函数的返回值为 0 如果流程成功并且 -1 如果失败,则设置错误条件。

在C中对ndarray进行子类型化

自2.2以来,python中潜藏的一个较不常用的特性是C中的子类类型的能力。此功能是基于C中已有的数字代码基的numpy的重要原因之一。C中的子类型在内存管理方面允许更大的灵活性。即使您对如何为Python创建新类型只有一个基本的了解,在C中进行子类型输入也不难。虽然从单个父类型进行子类型是最容易的,但也可以从多个父类型进行子类型。C中的多重继承通常不如Python有用,因为对Python子类型的限制是它们具有二进制兼容的内存布局。也许出于这个原因,从单个父类型进行子类型比较容易。

与Python对象对应的所有C结构必须以 PyObject_HEAD (或) PyObject_VAR_HEAD )以同样的方式,任何子类型都必须具有与父类型完全相同的内存布局(或者在多重继承的情况下,所有父类型)开头的C结构。这样做的原因是,python可能会尝试像访问父结构一样访问子类型结构的成员。( i.e. 它将把一个给定的指针强制转换为指向父结构的指针,然后取消对其中一个成员的引用)。如果内存布局不兼容,那么这种尝试将导致不可预测的行为(最终导致内存冲突和程序崩溃)。

中的一个元素 PyObject_HEAD 是指向类型对象结构的指针。通过创建一个新的类型对象结构并用函数和指针填充它来描述该类型所需的行为,可以创建一个新的python类型。通常,还会创建一个新的C结构来包含类型的每个对象所需的特定于实例的信息。例如, &PyArray_Type 是指向ndarray的类型对象表的指针,而 PyArrayObject* 变量是指向ndarray的特定实例的指针(ndarray结构的成员之一反过来是指向类型-对象表的指针 &PyArray_Type )终于 PyType_Ready 必须为每个新的python类型调用(<pointer_to_type_object>)。

创建子类型

要创建子类型,必须遵循类似的过程,除非不同的行为需要类型-对象结构中的新条目。所有其他条目都可以为空,并将由 PyType_Ready 具有来自父类型的适当函数。特别是,要在C中创建子类型,请执行以下步骤:

  1. 如果需要,请创建一个新的C结构来处理类型的每个实例。典型的C型结构为:

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

    请注意,完整的pyarrayObject用作第一个条目,以确保新类型实例的二进制布局与pyarrayObject相同。

  2. 用指向新函数的指针填充一个新的python类型对象结构,这些新函数将超越默认行为,同时保留任何应该保持不变的函数(或空)。tp_name元素应该不同。

  3. 用指向(主)父类型对象的指针填充新类型对象结构的tp_基成员。对于多重继承,还应使用包含所有父对象的元组填充tp_-bases成员,其顺序应用于定义继承。记住,为了使多个继承正常工作,所有父类型都必须具有相同的C结构。

  4. 呼叫 PyType_Ready (<pointer_to_new_type>)。如果此函数返回负数,则会发生故障,并且类型未初始化。否则,类型就可以使用了。通常重要的是将对新类型的引用放入模块字典中,以便可以从Python访问它。

有关在C中创建子类型的更多信息,可以通过阅读PEP253(可从https://www.python.org/dev/peps/pep-0253获得)。

ndarray子类型的特定特征

数组使用一些特殊的方法和属性,以便于子类型与基ndarray类型的互操作。

这个 __array_finalize__ 方法

ndarray.__array_finalize__

Ndarray的几个数组创建函数允许创建特定子类型的规范。这允许在许多例程中无缝地处理子类型。然而,当以这种方式创建子类型时,两者都不会 __new__ 方法也不 __init__ 方法被调用。相反,将分配子类型并填充适当的实例结构成员。最后, __array_finalize__ 属性在对象字典中查找。如果它存在而不是无,那么它可以是包含指向 PyArray_FinalizeFunc 或者它可以是采用单个参数的方法(可以是无参数)。

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

(int) (PyArrayObject *, PyObject *)

第一个参数是新创建的子类型。第二个参数(如果不是空值)是“parent”数组(如果该数组是使用切片或其他操作创建的,其中存在可清晰区分的父数组)。这个程序可以做任何它想做的事情。它应该在出错时返回-1,否则返回0。

如果 __array_finalize__ 属性既不是none也不是cobject,那么它必须是一个以父数组为参数(如果没有父数组,则可能是none)的python方法,并且不返回任何值。将捕获并处理此方法中的错误。

这个 __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,它还可以用于编写一个decorator,该decorator将只与ndarrays一起工作的函数转换为与任何类型一起工作的函数。 __array____array_wrap__ 方法。