数组迭代器 API#

数组迭代器#

数组迭代器封装了 ufunc 的许多关键特性,允许用户代码支持输出参数、内存布局的保留以及对未对齐或类型错误的数据进行缓冲,而无需进行复杂的编码。

此页面记录了迭代器的 API。迭代器的名称为 NpyIter,函数的名称为 NpyIter_*

对于使用此 C API 的用户,可能对数组迭代入门指南感兴趣。在许多情况下,最好先在 Python 中创建迭代器来测试想法,然后再编写 C 迭代代码。

迭代示例#

熟悉迭代器的最佳方法是查看它在 NumPy 代码库本身中的用法。例如,以下是 PyArray_CountNonzero 代码(用于计算数组中非零元素的数量)的略微修改版本。

npy_intp PyArray_CountNonzero(PyArrayObject* self)
{
    /* Nonzero boolean function */
    PyArray_NonzeroFunc* nonzero = PyArray_DESCR(self)->f->nonzero;

    NpyIter* iter;
    NpyIter_IterNextFunc *iternext;
    char** dataptr;
    npy_intp nonzero_count;
    npy_intp* strideptr,* innersizeptr;

    /* Handle zero-sized arrays specially */
    if (PyArray_SIZE(self) == 0) {
        return 0;
    }

    /*
     * Create and use an iterator to count the nonzeros.
     *   flag NPY_ITER_READONLY
     *     - The array is never written to.
     *   flag NPY_ITER_EXTERNAL_LOOP
     *     - Inner loop is done outside the iterator for efficiency.
     *   flag NPY_ITER_NPY_ITER_REFS_OK
     *     - Reference types are acceptable.
     *   order NPY_KEEPORDER
     *     - Visit elements in memory order, regardless of strides.
     *       This is good for performance when the specific order
     *       elements are visited is unimportant.
     *   casting NPY_NO_CASTING
     *     - No casting is required for this operation.
     */
    iter = NpyIter_New(self, NPY_ITER_READONLY|
                             NPY_ITER_EXTERNAL_LOOP|
                             NPY_ITER_REFS_OK,
                        NPY_KEEPORDER, NPY_NO_CASTING,
                        NULL);
    if (iter == NULL) {
        return -1;
    }

    /*
     * The iternext function gets stored in a local variable
     * so it can be called repeatedly in an efficient manner.
     */
    iternext = NpyIter_GetIterNext(iter, NULL);
    if (iternext == NULL) {
        NpyIter_Deallocate(iter);
        return -1;
    }
    /* The location of the data pointer which the iterator may update */
    dataptr = NpyIter_GetDataPtrArray(iter);
    /* The location of the stride which the iterator may update */
    strideptr = NpyIter_GetInnerStrideArray(iter);
    /* The location of the inner loop size which the iterator may update */
    innersizeptr = NpyIter_GetInnerLoopSizePtr(iter);

    nonzero_count = 0;
    do {
        /* Get the inner loop data/stride/count values */
        char* data = *dataptr;
        npy_intp stride = *strideptr;
        npy_intp count = *innersizeptr;

        /* This is a typical inner loop for NPY_ITER_EXTERNAL_LOOP */
        while (count--) {
            if (nonzero(data, self)) {
                ++nonzero_count;
            }
            data += stride;
        }

        /* Increment the iterator to the next inner loop */
    } while(iternext(iter));

    NpyIter_Deallocate(iter);

    return nonzero_count;
}

多迭代示例#

这是一个使用迭代器的复制函数。 order 参数用于控制已分配结果的内存布局,通常需要 NPY_KEEPORDER

PyObject *CopyArray(PyObject *arr, NPY_ORDER order)
{
    NpyIter *iter;
    NpyIter_IterNextFunc *iternext;
    PyObject *op[2], *ret;
    npy_uint32 flags;
    npy_uint32 op_flags[2];
    npy_intp itemsize, *innersizeptr, innerstride;
    char **dataptrarray;

    /*
     * No inner iteration - inner loop is handled by CopyArray code
     */
    flags = NPY_ITER_EXTERNAL_LOOP;
    /*
     * Tell the constructor to automatically allocate the output.
     * The data type of the output will match that of the input.
     */
    op[0] = arr;
    op[1] = NULL;
    op_flags[0] = NPY_ITER_READONLY;
    op_flags[1] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE;

    /* Construct the iterator */
    iter = NpyIter_MultiNew(2, op, flags, order, NPY_NO_CASTING,
                            op_flags, NULL);
    if (iter == NULL) {
        return NULL;
    }

    /*
     * Make a copy of the iternext function pointer and
     * a few other variables the inner loop needs.
     */
    iternext = NpyIter_GetIterNext(iter, NULL);
    innerstride = NpyIter_GetInnerStrideArray(iter)[0];
    itemsize = NpyIter_GetDescrArray(iter)[0]->elsize;
    /*
     * The inner loop size and data pointers may change during the
     * loop, so just cache the addresses.
     */
    innersizeptr = NpyIter_GetInnerLoopSizePtr(iter);
    dataptrarray = NpyIter_GetDataPtrArray(iter);

    /*
     * Note that because the iterator allocated the output,
     * it matches the iteration order and is packed tightly,
     * so we don't need to check it like the input.
     */
    if (innerstride == itemsize) {
        do {
            memcpy(dataptrarray[1], dataptrarray[0],
                                    itemsize * (*innersizeptr));
        } while (iternext(iter));
    } else {
        /* For efficiency, should specialize this based on item size... */
        npy_intp i;
        do {
            npy_intp size = *innersizeptr;
            char *src = dataptrarray[0], *dst = dataptrarray[1];
            for(i = 0; i < size; i++, src += innerstride, dst += itemsize) {
                memcpy(dst, src, itemsize);
            }
        } while (iternext(iter));
    }

    /* Get the result from the iterator object array */
    ret = NpyIter_GetOperandArray(iter)[1];
    Py_INCREF(ret);

    if (NpyIter_Deallocate(iter) != NPY_SUCCEED) {
        Py_DECREF(ret);
        return NULL;
    }

    return ret;
}

多索引跟踪示例#

此示例演示如何使用 NPY_ITER_MULTI_INDEX 标志。为简便起见,我们假设参数是一个二维数组。

int PrintMultiIndex(PyArrayObject *arr) {
    NpyIter *iter;
    NpyIter_IterNextFunc *iternext;
    npy_intp multi_index[2];

    iter = NpyIter_New(
        arr, NPY_ITER_READONLY | NPY_ITER_MULTI_INDEX | NPY_ITER_REFS_OK,
        NPY_KEEPORDER, NPY_NO_CASTING, NULL);
    if (iter == NULL) {
        return -1;
    }
    if (NpyIter_GetNDim(iter) != 2) {
        NpyIter_Deallocate(iter);
        PyErr_SetString(PyExc_ValueError, "Array must be 2-D");
        return -1;
    }
    if (NpyIter_GetIterSize(iter) != 0) {
        iternext = NpyIter_GetIterNext(iter, NULL);
        if (iternext == NULL) {
            NpyIter_Deallocate(iter);
            return -1;
        }
        NpyIter_GetMultiIndexFunc *get_multi_index =
            NpyIter_GetGetMultiIndex(iter, NULL);
        if (get_multi_index == NULL) {
            NpyIter_Deallocate(iter);
            return -1;
        }

        do {
            get_multi_index(iter, multi_index);
            printf("multi_index is [%" NPY_INTP_FMT ", %" NPY_INTP_FMT "]\n",
                   multi_index[0], multi_index[1]);
        } while (iternext(iter));
    }
    if (!NpyIter_Deallocate(iter)) {
        return -1;
    }
    return 0;
}

当使用 2x3 数组调用时,上面的示例将打印

multi_index is [0, 0]
multi_index is [0, 1]
multi_index is [0, 2]
multi_index is [1, 0]
multi_index is [1, 1]
multi_index is [1, 2]

迭代器数据类型#

迭代器布局是内部细节,用户代码只能看到不完整的结构。

type NpyIter#

这是迭代器的非透明指针类型。只能通过迭代器 API 访问其内容。

type NpyIter_Type#

这是将迭代器公开给 Python 的类型。目前,没有公开的 API 可以访问 Python 创建的迭代器的值。如果在 Python 中创建迭代器,则必须在 Python 中使用它,反之亦然。此类 API 可能会在未来的版本中创建。

type NpyIter_IterNextFunc#

这是迭代循环的函数指针,由 NpyIter_GetIterNext 返回。

type NpyIter_GetMultiIndexFunc#

这是一个用于获取当前迭代器多索引的函数指针,由 NpyIter_GetGetMultiIndex 返回。

构造和析构#

NpyIter *NpyIter_New(PyArrayObject *op, npy_uint32 flags, NPY_ORDER order, NPY_CASTING casting, PyArray_Descr *dtype)#

为给定的 numpy 数组对象 op 创建迭代器。

可在 flags 中传递的标志是 NpyIter_MultiNew 中记录的全局标志和每个操作数标志的任意组合,但 NPY_ITER_ALLOCATE 除外。

可以将任何 NPY_ORDER 枚举值传递给 order。为了高效迭代, NPY_KEEPORDER 是最佳选择,其他顺序强制执行特定的迭代模式。

可以将任何 NPY_CASTING 枚举值传递给 casting。这些值包括 NPY_NO_CASTINGNPY_EQUIV_CASTINGNPY_SAFE_CASTINGNPY_SAME_KIND_CASTINGNPY_UNSAFE_CASTING。要允许进行强制转换,还必须启用复制或缓冲。

如果 dtype 不是 NULL,则需要该数据类型。如果允许复制,则如果数据可转换,它将创建一个临时副本。如果启用了 NPY_ITER_UPDATEIFCOPY,它还将在迭代器销毁时使用另一个强制转换复制回数据。

如果出错则返回 NULL,否则返回已分配的迭代器。

要创建一个类似于旧迭代器的迭代器,这应该有效。

iter = NpyIter_New(op, NPY_ITER_READWRITE,
                    NPY_CORDER, NPY_NO_CASTING, NULL);

如果要使用对齐的 double 代码编辑数组,但顺序无关紧要,则可以使用此方法。

dtype = PyArray_DescrFromType(NPY_DOUBLE);
iter = NpyIter_New(op, NPY_ITER_READWRITE|
                    NPY_ITER_BUFFERED|
                    NPY_ITER_NBO|
                    NPY_ITER_ALIGNED,
                    NPY_KEEPORDER,
                    NPY_SAME_KIND_CASTING,
                    dtype);
Py_DECREF(dtype);
NpyIter *NpyIter_MultiNew(npy_intp nop, PyArrayObject **op, npy_uint32 flags, NPY_ORDER order, NPY_CASTING casting, npy_uint32 *op_flags, PyArray_Descr **op_dtypes)#

创建一个迭代器,用于根据 NumPy 的广播规则广播在 op 中提供的 nop 个数组对象。

任何 NPY_ORDER 枚举值都可以传递给 order。为了高效迭代,NPY_KEEPORDER 是最佳选择,其他顺序则强制执行特定的迭代模式。当使用 NPY_KEEPORDER 时,如果还希望确保沿轴的迭代不反转,则应传递标志 NPY_ITER_DONT_NEGATE_STRIDES

可以将任何 NPY_CASTING 枚举值传递给 casting。这些值包括 NPY_NO_CASTINGNPY_EQUIV_CASTINGNPY_SAFE_CASTINGNPY_SAME_KIND_CASTINGNPY_UNSAFE_CASTING。要允许进行强制转换,还必须启用复制或缓冲。

如果 op_dtypes 不是 NULL,它将为每个 op[i] 指定一个数据类型或 NULL

如果出错则返回 NULL,否则返回已分配的迭代器。

可以传递到 flags 中的标志应用于整个迭代器,它们是:

NPY_ITER_C_INDEX#

使迭代器跟踪与 C 顺序匹配的展平的平面索引。此选项不能与 NPY_ITER_F_INDEX 一起使用。

NPY_ITER_F_INDEX#

使迭代器跟踪与 Fortran 顺序匹配的展平的平面索引。此选项不能与 NPY_ITER_C_INDEX 一起使用。

NPY_ITER_MULTI_INDEX#

使迭代器跟踪多重索引。这将阻止迭代器合并轴以生成更大的内部循环。如果循环也没有被缓冲并且没有跟踪索引(可以调用 NpyIter_RemoveAxis),则迭代器大小可以为 -1 以指示迭代器太大。这可能是由于复杂的广播造成的,并且会在设置迭代器范围、删除多重索引或获取下一个函数时导致错误的创建。但是,如果在移除后大小足够小,则可以再次移除轴并正常使用迭代器。

NPY_ITER_EXTERNAL_LOOP#

使迭代器跳过最内层循环的迭代,要求迭代器的用户来处理它。

此标志与 NPY_ITER_C_INDEXNPY_ITER_F_INDEXNPY_ITER_MULTI_INDEX 不兼容。

NPY_ITER_DONT_NEGATE_STRIDES#

这仅在为 order 参数指定 NPY_KEEPORDER 时影响迭代器。默认情况下,使用 NPY_KEEPORDER,迭代器会反转步长为负的轴,以便向前遍历内存。这将禁用此步骤。如果要使用轴的基础内存顺序,但不想反转轴,请使用此标志。例如,这就是 numpy.ravel(a, order='K') 的行为。

NPY_ITER_COMMON_DTYPE#

使迭代器将所有操作数转换为一个公共数据类型,该类型是根据 ufunc 类型提升规则计算的。必须启用复制或缓冲。

如果提前知道公共数据类型,则不要使用此标志。而是为所有操作数设置请求的 dtype。

NPY_ITER_REFS_OK#

指示可以接受和使用具有引用类型的数组(包含对象类型的对象数组或结构化数组)在迭代器中。如果启用此标志,则调用者必须确保检查 NpyIter_IterationNeedsAPI(iter) 是否为 true,在这种情况下,它可能不会在迭代期间释放 GIL。

NPY_ITER_ZEROSIZE_OK#

指示应允许大小为零的数组。由于典型的迭代循环不能自然地处理大小为零的数组,因此您必须在进入迭代循环之前检查 IterSize 是否大于零。目前只检查操作数,而不是强制形状。

NPY_ITER_REDUCE_OK#

允许具有零步长和大小大于一的维度的可写操作数。请注意,此类操作数必须是读/写操作。

启用缓冲时,这也切换到一种特殊的缓冲模式,该模式会根据需要减少循环长度,以免践踏正在减少的值。

请注意,如果要对自动分配的输出进行约简,则必须使用 NpyIter_GetOperandArray 获取其引用,然后在执行迭代循环之前将每个值设置为约简单元。对于缓冲约简,这意味着您还必须指定标志 NPY_ITER_DELAY_BUFALLOC,然后在初始化分配的操作数以准备缓冲区后重置迭代器。

NPY_ITER_RANGED#

启用对完整 iterindex 范围 [0, NpyIter_IterSize(iter)) 的子范围进行迭代的支持。使用函数 NpyIter_ResetToIterIndexRange 指定迭代范围。

此标志只能与 NPY_ITER_EXTERNAL_LOOP 一起使用,前提是启用了 NPY_ITER_BUFFERED。这是因为如果没有缓冲,内循环始终是最内层迭代维的大小,并且允许将其分割需要特殊处理,实际上使其更像缓冲版本。

NPY_ITER_BUFFERED#

使迭代器存储缓冲数据,并使用缓冲来满足数据类型、对齐和字节顺序要求。要缓冲操作数,不要指定 NPY_ITER_COPYNPY_ITER_UPDATEIFCOPY 标志,因为它们会覆盖缓冲。缓冲对于使用迭代器的 Python 代码特别有用,因为它允许一次处理更大块的数据来摊销 Python 解释器的开销。

如果与NPY_ITER_EXTERNAL_LOOP一起使用,由于步幅的布局方式,调用方的内循环可能会获得比没有缓冲时更大的块。

请注意,如果一个操作数被赋予标志NPY_ITER_COPYNPY_ITER_UPDATEIFCOPY,则会优先进行复制而不是缓冲。当数组被广播时,仍然会发生缓冲,因为需要复制元素以获得恒定的步幅。

在正常的缓冲中,每个内循环的大小等于缓冲区大小,或者如果指定了NPY_ITER_GROWINNER,则可能更大。如果启用了NPY_ITER_REDUCE_OK并且发生了归约,则内循环可能会根据归约的结构变小。

NPY_ITER_GROWINNER#

启用缓冲后,当不需要缓冲时,这允许内循环的大小增长。如果您正在对所有数据进行直接传递,而不是对每个内循环使用小的缓存友好型临时值数组,则此选项最有效。

NPY_ITER_DELAY_BUFALLOC#

启用缓冲后,这会延迟缓冲区的分配,直到调用NpyIter_Reset或其他重置函数。此标志的存在是为了避免在为多线程迭代创建多个缓冲迭代器的副本时浪费地复制缓冲区数据。

此标志的另一个用途是设置归约操作。创建迭代器后,并由迭代器自动分配归约输出(务必使用READWRITE访问权限),其值可以初始化为归约单元。使用NpyIter_GetOperandArray获取对象。然后,调用NpyIter_Reset来分配并用其初始值填充缓冲区。

NPY_ITER_COPY_IF_OVERLAP#

如果任何写操作数与任何读操作数重叠,则通过创建临时副本(如有必要,为写操作数启用UPDATEIFCOPY)来消除所有重叠。如果存在包含两个数组公共数据的内存地址,则一对操作数重叠。

由于精确重叠检测在维度数量上的运行时间呈指数级增长,因此该决定是基于启发式算法做出的,这会导致误报(在异常情况下不必要的复制),但没有漏报。

如果存在任何读/写重叠,此标志确保操作的结果与所有操作数都被复制时的结果相同。在需要进行复制的情况下,**如果没有此标志,计算结果可能未定义!**

可在op_flags[i]中传递的标志,其中0 <= i < nop

NPY_ITER_READWRITE#
NPY_ITER_READONLY#
NPY_ITER_WRITEONLY#

指示迭代器的用户将如何读取或写入op[i]。每个操作数必须精确指定其中一个标志。对于用户提供的操作数,使用NPY_ITER_READWRITENPY_ITER_WRITEONLY可能会触发WRITEBACKIFCOPY语义。调用NpyIter_Deallocate时,数据将被写回原始数组。

NPY_ITER_COPY#

如果op[i]不满足构造函数标志和参数指定的的数据类型或对齐要求,则允许对其进行复制。

NPY_ITER_UPDATEIFCOPY#

触发NPY_ITER_COPY,当标记为写入的数组操作数被复制时,在调用NpyIter_Deallocate时,会将副本中的数据复制回op[i]

如果操作数被标记为只写,并且需要复制,则会创建一个未初始化的临时数组,然后在调用NpyIter_Deallocate时将其复制回op[i],而不是进行不必要的复制操作。

NPY_ITER_NBO#
NPY_ITER_ALIGNED#
NPY_ITER_CONTIG#

使迭代器提供op[i]的数据,该数据以本机字节序、根据dtype要求对齐、连续或任意组合的方式存在。

默认情况下,迭代器会生成指向提供的数组的指针,这些指针可能是对齐的或未对齐的,并且具有任何字节序。如果未启用复制或缓冲,并且操作数数据不满足约束条件,则会引发错误。

连续性约束仅适用于内循环,连续的内循环可能有任意的指针更改。

如果请求的数据类型为非本机字节序,则NBO标志会覆盖它,并将请求的数据类型转换为本机字节序。

NPY_ITER_ALLOCATE#

这是用于输出数组的,并要求设置标志NPY_ITER_WRITEONLYNPY_ITER_READWRITE。如果op[i]为NULL,则创建一个具有最终广播维度和与迭代器迭代顺序匹配的布局的新数组。

op[i]为NULL时,请求的数据类型op_dtypes[i]也可能为NULL,在这种情况下,它会根据标记为可读的数组的dtypes自动生成。生成dtype的规则与UFuncs相同。特别需要注意的是在所选dtype中处理字节序。如果只有一个输入,则按原样使用输入的dtype。否则,如果组合了多个输入dtypes,则输出将为本机字节序。

使用此标志分配后,调用者可以通过调用NpyIter_GetOperandArray并获取返回的C数组中的第i个对象来检索新数组。调用者必须对其调用Py_INCREF以声明对该数组的引用。

NPY_ITER_NO_SUBTYPE#

用于NPY_ITER_ALLOCATE,此标志禁用为输出分配数组子类型,强制其为直接ndarray。

待办事项:也许最好引入一个函数NpyIter_GetWrappedOutput并删除此标志?

NPY_ITER_NO_BROADCAST#

确保输入或输出与迭代维度完全匹配。

NPY_ITER_ARRAYMASK#

指示此操作数是在写入应用了NPY_ITER_WRITEMASKED标志的操作数时用于选择元素的掩码。只有一个操作数可以应用NPY_ITER_ARRAYMASK标志。

具有此标志的操作数的数据类型应为NPY_BOOLNPY_MASK或其字段均为有效掩码类型的结构体dtype。在后一种情况下,它必须与被WRITEMASKED的结构体操作数匹配,因为它为该数组的每个字段指定了一个掩码。

此标志仅影响将缓冲区中的数据写回数组。这意味着,如果操作数也是NPY_ITER_READWRITENPY_ITER_WRITEONLY,则进行迭代的代码可以写入此操作数以控制哪些元素将保持不变,哪些元素将被修改。当掩码应该是输入掩码的组合时,这非常有用。

NPY_ITER_WRITEMASKED#

此数组是所有writemasked操作数的掩码。代码使用writemasked标志,该标志指示仅写入所选ARRAYMASK操作数为True的元素。通常,迭代器不强制执行此操作,由进行迭代的代码来遵守该承诺。

当使用writemasked标志,并且此操作数被缓冲时,这会改变数据从缓冲区复制到数组的方式。会使用掩码复制例程,该例程仅复制缓冲区中writemasked从ARRAYMASK操作数的对应元素返回true的元素。

NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE#

在内存重叠检查中,假设启用了NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE的操作数仅按迭代器顺序访问。

这使迭代器能够推断数据依赖性,从而可能避免不必要的复制。

此标志仅在迭代器上启用了NPY_ITER_COPY_IF_OVERLAP时才有效。

NpyIter *NpyIter_AdvancedNew(npy_intp nop, PyArrayObject **op, npy_uint32 flags, NPY_ORDER order, NPY_CASTING casting, npy_uint32 *op_flags, PyArray_Descr **op_dtypes, int oa_ndim, int **op_axes, npy_intp const *itershape, npy_intp buffersize)#

扩展了NpyIter_MultiNew,提供了一些高级选项,可以更好地控制广播和缓冲。

如果将-1/NULL值传递给oa_ndimop_axesitershapebuffersize,则等效于NpyIter_MultiNew

oa_ndim参数,当不为零或-1时,指定将使用自定义广播迭代的维度数。如果提供了它,则必须提供op_axes,也可以提供itershapeop_axes参数允许您详细控制操作数数组的轴如何匹配在一起并进行迭代。在op_axes中,您必须提供一个指向oa_ndim大小的、类型为npy_intp的数组的nop个指针。如果op_axes中的条目为NULL,则将应用正常的广播规则。op_axes[j][i]中存储的是op[j]的有效轴,或者-1,表示newaxis。在每个op_axes[j]数组中,不能重复轴。以下示例说明了正常的广播如何应用于3D数组、2D数组、1D数组和标量。

注意:在NumPy 1.8之前,oa_ndim == 0用于表示op_axesitershape未使用。这已弃用,应替换为-1。对于这种情况,使用NpyIter_MultiNew可以获得更好的向后兼容性。

int oa_ndim = 3;               /* # iteration axes */
int op0_axes[] = {0, 1, 2};    /* 3-D operand */
int op1_axes[] = {-1, 0, 1};   /* 2-D operand */
int op2_axes[] = {-1, -1, 0};  /* 1-D operand */
int op3_axes[] = {-1, -1, -1}  /* 0-D (scalar) operand */
int* op_axes[] = {op0_axes, op1_axes, op2_axes, op3_axes};

itershape参数允许您强制迭代器具有特定的迭代形状。它是一个长度为oa_ndim的数组。当条目为负数时,其值由操作数确定。此参数允许自动分配的输出获得与任何输入维度不匹配的额外维度。

如果buffersize为零,则使用默认缓冲区大小,否则它指定要使用的缓冲区大小。建议使用2的幂次方作为缓冲区大小,例如4096或8192。

如果出错则返回 NULL,否则返回已分配的迭代器。

NpyIter *NpyIter_Copy(NpyIter *iter)#

创建给定迭代器的副本。此函数主要用于启用数据的多线程迭代。

待办事项:将其移动到有关多线程迭代的部分。

多线程迭代的推荐方法是:首先使用标志NPY_ITER_EXTERNAL_LOOPNPY_ITER_RANGEDNPY_ITER_BUFFEREDNPY_ITER_DELAY_BUFALLOC,以及可能NPY_ITER_GROWINNER创建迭代器。为每个线程创建一个此迭代器的副本(第一个迭代器除外)。然后,获取迭代索引范围[0, NpyIter_GetIterSize(iter))并将其分割成任务,例如使用TBB parallel_for循环。当线程获得要执行的任务时,它通过调用NpyIter_ResetToIterIndexRange并遍历整个范围来使用其迭代器副本。

在多线程代码或不持有Python GIL的代码中使用迭代器时,必须注意只调用在此上下文中安全的函数。NpyIter_Copy不能在没有Python GIL的情况下安全地调用,因为它会增加Python引用。Reset*和其他一些函数可以通过将errmsg参数作为非NULL值传递来安全地调用,以便函数通过它传递错误而不是设置Python异常。

必须为每个副本调用NpyIter_Deallocate

int NpyIter_RemoveAxis(NpyIter *iter, int axis)#

从迭代中移除一个轴。这要求在创建迭代器时设置了NPY_ITER_MULTI_INDEX,如果启用了缓冲或正在跟踪索引,则此方法无效。此函数还会将迭代器重置为其初始状态。

这对于设置累加循环很有用,例如,迭代器可以首先创建所有维度,包括累加轴,以便正确创建输出。然后,可以移除累加轴,并以嵌套方式进行计算。

警告:此函数可能会更改迭代器的内部内存布局。迭代器的任何缓存函数或指针都必须重新获取!迭代器范围也将被重置。

返回 NPY_SUCCEEDNPY_FAIL

int NpyIter_RemoveMultiIndex(NpyIter *iter)#

如果迭代器正在跟踪多索引,则此函数会去除对多索引的支持,并在不需要多索引的情况下进行进一步的迭代器优化。此函数还会将迭代器重置为其初始状态。

警告:此函数可能会更改迭代器的内部内存布局。迭代器的任何缓存函数或指针都必须重新获取!

调用此函数后,NpyIter_HasMultiIndex(iter) 将返回 false。

返回 NPY_SUCCEEDNPY_FAIL

int NpyIter_EnableExternalLoop(NpyIter *iter)#

如果调用了 NpyIter_RemoveMultiIndex,您可能需要启用标志 NPY_ITER_EXTERNAL_LOOP。此标志不允许与 NPY_ITER_MULTI_INDEX 一起使用,因此提供此函数以便在调用 NpyIter_RemoveMultiIndex 后启用此功能。此函数还会将迭代器重置为其初始状态。

警告:此函数会更改迭代器的内部逻辑。迭代器的任何缓存函数或指针都必须重新获取!

返回 NPY_SUCCEEDNPY_FAIL

int NpyIter_Deallocate(NpyIter *iter)#

释放迭代器对象并解决任何需要的写回操作。

返回 NPY_SUCCEEDNPY_FAIL

int NpyIter_Reset(NpyIter *iter, char **errmsg)#

将迭代器重置回其初始状态,位于迭代范围的开头。

返回 NPY_SUCCEEDNPY_FAIL。如果 errmsg 不为 NULL,则在返回 NPY_FAIL 时不会设置 Python 异常。相反,*errmsg 将设置为错误消息。当 errmsg 不为 NULL 时,可以在不持有 Python GIL 的情况下安全地调用此函数。

int NpyIter_ResetToIterIndexRange(NpyIter *iter, npy_intp istart, npy_intp iend, char **errmsg)#

重置迭代器并将其限制在 iterindex 范围 [istart, iend) 内。有关如何将其用于多线程迭代的说明,请参见 NpyIter_Copy。这要求将标志 NPY_ITER_RANGED 传递给迭代器构造函数。

如果您想同时重置 iterindex 范围和基指针,您可以执行以下操作以避免额外的缓冲区复制(复制此代码时,请务必添加返回代码错误检查)。

/* Set to a trivial empty range */
NpyIter_ResetToIterIndexRange(iter, 0, 0);
/* Set the base pointers */
NpyIter_ResetBasePointers(iter, baseptrs);
/* Set to the desired range */
NpyIter_ResetToIterIndexRange(iter, istart, iend);

返回 NPY_SUCCEEDNPY_FAIL。如果 errmsg 不为 NULL,则在返回 NPY_FAIL 时不会设置 Python 异常。相反,*errmsg 将设置为错误消息。当 errmsg 不为 NULL 时,可以在不持有 Python GIL 的情况下安全地调用此函数。

int NpyIter_ResetBasePointers(NpyIter *iter, char **baseptrs, char **errmsg)#

将迭代器重置回其初始状态,但使用 baseptrs 中的值作为数据,而不是正在迭代的数组的指针。此函数旨在与 op_axes 参数一起由具有两个或多个迭代器的嵌套迭代代码使用。

返回 NPY_SUCCEEDNPY_FAIL。如果 errmsg 不为 NULL,则在返回 NPY_FAIL 时不会设置 Python 异常。相反,*errmsg 将设置为错误消息。当 errmsg 不为 NULL 时,可以在不持有 Python GIL 的情况下安全地调用此函数。

待办事项:将以下内容移至嵌套迭代器的特殊部分。

创建用于嵌套迭代的迭代器需要一些小心。所有迭代器操作数必须完全匹配,否则对 NpyIter_ResetBasePointers 的调用将无效。这意味着不应随意使用自动复制和输出分配。可以通过使用启用了所有转换参数的迭代器之一来创建迭代器,然后使用 NpyIter_GetOperandArray 函数获取已分配的操作数并将它们传递给其余迭代器的构造函数,从而仍然可以使用迭代器的自动数据转换和转换功能。

警告:创建用于嵌套迭代的迭代器时,代码不得在不同的迭代器中多次使用一个维度。如果这样做,嵌套迭代将在迭代期间产生越界指针。

警告:创建用于嵌套迭代的迭代器时,缓冲只能应用于最内层的迭代器。如果将缓冲迭代器用作 baseptrs 的源,它将指向一个小缓冲区而不是数组,并且内部迭代将无效。

使用嵌套迭代器的模式如下所示。

NpyIter *iter1, *iter1;
NpyIter_IterNextFunc *iternext1, *iternext2;
char **dataptrs1;

/*
 * With the exact same operands, no copies allowed, and
 * no axis in op_axes used both in iter1 and iter2.
 * Buffering may be enabled for iter2, but not for iter1.
 */
iter1 = ...; iter2 = ...;

iternext1 = NpyIter_GetIterNext(iter1);
iternext2 = NpyIter_GetIterNext(iter2);
dataptrs1 = NpyIter_GetDataPtrArray(iter1);

do {
    NpyIter_ResetBasePointers(iter2, dataptrs1);
    do {
        /* Use the iter2 values */
    } while (iternext2(iter2));
} while (iternext1(iter1));
int NpyIter_GotoMultiIndex(NpyIter *iter, npy_intp const *multi_index)#

调整迭代器以指向 multi_index 指向的 ndim 索引。如果未跟踪多索引、索引超出范围或禁用了内部循环迭代,则返回错误。

返回 NPY_SUCCEEDNPY_FAIL

int NpyIter_GotoIndex(NpyIter *iter, npy_intp index)#

调整迭代器以指向指定的 index。如果使用标志 NPY_ITER_C_INDEX 构造迭代器,则 index 是 C 序索引;如果使用标志 NPY_ITER_F_INDEX 构造迭代器,则 index 是 Fortran 序索引。如果没有跟踪索引、索引超出范围或禁用了内部循环迭代,则返回错误。

返回 NPY_SUCCEEDNPY_FAIL

npy_intp NpyIter_GetIterSize(NpyIter *iter)#

返回正在迭代的元素数量。这是形状中所有维度的乘积。当正在跟踪多重索引(并且可能调用NpyIter_RemoveAxis)时,大小可能为-1,表示迭代器太大。此类迭代器无效,但在调用NpyIter_RemoveAxis之后可能会变为有效。无需检查这种情况。

npy_intp NpyIter_GetIterIndex(NpyIter *iter)#

获取迭代器的iterindex,这是一个与迭代器的迭代顺序匹配的索引。

void NpyIter_GetIterIndexRange(NpyIter *iter, npy_intp *istart, npy_intp *iend)#

获取正在迭代的iterindex子范围。如果没有指定NPY_ITER_RANGED,则始终返回范围[0, NpyIter_IterSize(iter))

int NpyIter_GotoIterIndex(NpyIter *iter, npy_intp iterindex)#

调整迭代器以指向指定的iterindex。IterIndex 是与迭代器的迭代顺序匹配的索引。如果iterindex超出范围、启用了缓冲或禁用了内部循环迭代,则返回错误。

返回 NPY_SUCCEEDNPY_FAIL

npy_bool NpyIter_HasDelayedBufAlloc(NpyIter *iter)#

如果将标志NPY_ITER_DELAY_BUFALLOC传递给迭代器构造函数,并且尚未对重置函数进行任何调用,则返回1;否则返回0。

npy_bool NpyIter_HasExternalLoop(NpyIter *iter)#

如果调用者需要处理最内层的1维循环,则返回1;如果迭代器处理所有循环,则返回0。这由构造函数标志NPY_ITER_EXTERNAL_LOOPNpyIter_EnableExternalLoop控制。

npy_bool NpyIter_HasMultiIndex(NpyIter *iter)#

如果使用NPY_ITER_MULTI_INDEX标志创建迭代器,则返回1;否则返回0。

npy_bool NpyIter_HasIndex(NpyIter *iter)#

如果使用NPY_ITER_C_INDEXNPY_ITER_F_INDEX标志创建迭代器,则返回1;否则返回0。

npy_bool NpyIter_RequiresBuffering(NpyIter *iter)#

如果迭代器需要缓冲,则返回1;当操作数需要转换或对齐而无法直接使用时,就会发生这种情况。

npy_bool NpyIter_IsBuffered(NpyIter *iter)#

如果使用NPY_ITER_BUFFERED标志创建迭代器,则返回1;否则返回0。

npy_bool NpyIter_IsGrowInner(NpyIter *iter)#

如果使用NPY_ITER_GROWINNER标志创建迭代器,则返回1;否则返回0。

npy_intp NpyIter_GetBufferSize(NpyIter *iter)#

如果迭代器已缓冲,则返回正在使用的缓冲区的大小;否则返回0。

int NpyIter_GetNDim(NpyIter *iter)#

返回正在迭代的维度数。如果在迭代器构造函数中没有请求多重索引,则此值可能小于原始对象中的维度数。

int NpyIter_GetNOp(NpyIter *iter)#

返回迭代器中操作数的数量。

npy_intp *NpyIter_GetAxisStrideArray(NpyIter *iter, int axis)#

获取指定轴的步幅数组。要求迭代器正在跟踪多重索引,并且未启用缓冲。

当您想以某种方式匹配操作数轴,然后使用NpyIter_RemoveAxis手动处理它们的处理时,可以使用此方法。在移除轴之前调用此函数,您可以获得手动处理的步幅。

出错时返回NULL

int NpyIter_GetShape(NpyIter *iter, npy_intp *outshape)#

outshape中返回迭代器的广播形状。这只能在跟踪多重索引的迭代器上调用。

返回 NPY_SUCCEEDNPY_FAIL

PyArray_Descr **NpyIter_GetDescrArray(NpyIter *iter)#

此函数返回一个指向正在迭代的对象的`nop`数据类型Descrs的指针。结果指向`iter`内部,因此调用者不会获得对Descrs的任何引用。

此指针可以在迭代循环之前缓存,调用`iternext`不会改变它。

PyObject **NpyIter_GetOperandArray(NpyIter *iter)#

此函数返回一个指向正在迭代的`nop`操作数PyObjects的指针。结果指向`iter`内部,因此调用者不会获得对PyObjects的任何引用。

PyObject *NpyIter_GetIterView(NpyIter *iter, npy_intp i)#

此函数返回对一个新的ndarray视图的引用,该视图是数组NpyIter_GetOperandArray中第i个对象的视图,其维度和步幅与内部优化的迭代模式匹配。此视图的C序迭代等效于迭代器的迭代顺序。

例如,如果使用单个数组作为输入创建迭代器,并且可以重新排列其所有轴然后将其折叠成单个步幅迭代,则此函数将返回一个一维数组的视图。

void NpyIter_GetReadFlags(NpyIter *iter, char *outreadflags)#

填充`nop`标志。如果可以从`op[i]`读取,则将`outreadflags[i]`设置为1;否则设置为0。

void NpyIter_GetWriteFlags(NpyIter *iter, char *outwriteflags)#

填充`nop`标志。如果可以写入`op[i]`,则将`outwriteflags[i]`设置为1;否则设置为0。

int NpyIter_CreateCompatibleStrides(NpyIter *iter, npy_intp itemsize, npy_intp *outstrides)#

构建一组步幅,这些步幅与使用NPY_ITER_ALLOCATE标志创建的输出数组的步幅相同,其中op_axes传递NULL。这是用于连续打包的数据,但不一定是C或Fortran顺序。这应该与NpyIter_GetShapeNpyIter_GetNDim一起使用,并将NPY_ITER_MULTI_INDEX标志传递到构造函数。

此函数的一个用例是匹配迭代器的形状和布局,并添加一个或多个维度。例如,为了为数值梯度生成每个输入值的向量,您将ndim*itemsize传递给itemsize,然后添加另一个大小为ndim且步幅为itemsize的维度到末尾。要进行Hessian矩阵,您可以执行相同的操作,但添加两个维度,或者利用对称性并将其打包到具有特定编码的1个维度中。

只有当迭代器正在跟踪多索引并且使用了NPY_ITER_DONT_NEGATE_STRIDES来防止轴反向迭代时,才能调用此函数。

如果使用此方法创建数组,则只需为每次迭代添加“itemsize”即可遍历与迭代器匹配的新数组。

返回 NPY_SUCCEEDNPY_FAIL

npy_bool NpyIter_IsFirstVisit(NpyIter *iter, int iop)#

检查这是第一次看到迭代器指向的指定约简操作数的元素。对于约简操作数和禁用缓冲区的情况,此函数返回合理的答案。对于缓冲的非约简操作数,答案可能不正确。

此函数旨在仅在EXTERNAL_LOOP模式下使用,并且在未启用该模式时会产生一些错误的答案。

如果此函数返回true,调用者还应该检查操作数的内循环步幅,因为如果该步幅为0,则只有最内层外部循环的第一个元素是第一次被访问。

警告:出于性能原因,“iop”没有边界检查,没有确认“iop”实际上是一个约简操作数,也没有确认已启用EXTERNAL_LOOP模式。这些检查是调用者的责任,应该在任何内部循环之外进行。

迭代函数#

NpyIter_IterNextFunc *NpyIter_GetIterNext(NpyIter *iter, char **errmsg)#

返回一个用于迭代的函数指针。此函数可能会计算函数指针的专用版本,而不是将其存储在迭代器结构中。因此,为了获得良好的性能,需要将函数指针保存在变量中,而不是在每次循环迭代中检索。

如果发生错误,则返回NULL。如果errmsg非NULL,则返回NPY_FAIL时不会设置Python异常。而是将*errmsg设置为错误消息。当errmsg非NULL时,可以在不持有Python GIL的情况下安全地调用此函数。

典型的循环结构如下所示。

NpyIter_IterNextFunc *iternext = NpyIter_GetIterNext(iter, NULL);
char** dataptr = NpyIter_GetDataPtrArray(iter);

do {
    /* use the addresses dataptr[0], ... dataptr[nop-1] */
} while(iternext(iter));

当指定NPY_ITER_EXTERNAL_LOOP时,典型的内循环结构如下所示。

NpyIter_IterNextFunc *iternext = NpyIter_GetIterNext(iter, NULL);
char** dataptr = NpyIter_GetDataPtrArray(iter);
npy_intp* stride = NpyIter_GetInnerStrideArray(iter);
npy_intp* size_ptr = NpyIter_GetInnerLoopSizePtr(iter), size;
npy_intp iop, nop = NpyIter_GetNOp(iter);

do {
    size = *size_ptr;
    while (size--) {
        /* use the addresses dataptr[0], ... dataptr[nop-1] */
        for (iop = 0; iop < nop; ++iop) {
            dataptr[iop] += stride[iop];
        }
    }
} while (iternext());

请注意,我们正在使用迭代器内的dataptr数组,而不是将值复制到本地临时变量。这是可能的,因为当调用iternext()时,这些指针将被新的值覆盖,而不是增量更新。

如果正在使用编译时固定缓冲区(NPY_ITER_BUFFEREDNPY_ITER_EXTERNAL_LOOP两个标志),则大小也可以用作信号。当iternext()返回false时,大小保证变为零,从而启用以下循环结构。请注意,如果您使用此结构,则不应将NPY_ITER_GROWINNER作为标志传递,因为它会在某些情况下导致更大的大小。

/* The constructor should have buffersize passed as this value */
#define FIXED_BUFFER_SIZE 1024

NpyIter_IterNextFunc *iternext = NpyIter_GetIterNext(iter, NULL);
char **dataptr = NpyIter_GetDataPtrArray(iter);
npy_intp *stride = NpyIter_GetInnerStrideArray(iter);
npy_intp *size_ptr = NpyIter_GetInnerLoopSizePtr(iter), size;
npy_intp i, iop, nop = NpyIter_GetNOp(iter);

/* One loop with a fixed inner size */
size = *size_ptr;
while (size == FIXED_BUFFER_SIZE) {
    /*
     * This loop could be manually unrolled by a factor
     * which divides into FIXED_BUFFER_SIZE
     */
    for (i = 0; i < FIXED_BUFFER_SIZE; ++i) {
        /* use the addresses dataptr[0], ... dataptr[nop-1] */
        for (iop = 0; iop < nop; ++iop) {
            dataptr[iop] += stride[iop];
        }
    }
    iternext();
    size = *size_ptr;
}

/* Finish-up loop with variable inner size */
if (size > 0) do {
    size = *size_ptr;
    while (size--) {
        /* use the addresses dataptr[0], ... dataptr[nop-1] */
        for (iop = 0; iop < nop; ++iop) {
            dataptr[iop] += stride[iop];
        }
    }
} while (iternext());
NpyIter_GetMultiIndexFunc *NpyIter_GetGetMultiIndex(NpyIter *iter, char **errmsg)#

返回一个用于获取迭代器当前多维索引的函数指针。如果迭代器未跟踪多维索引,则返回 NULL。建议在迭代循环之前将此函数指针缓存在局部变量中。

如果发生错误,则返回NULL。如果errmsg非NULL,则返回NPY_FAIL时不会设置Python异常。而是将*errmsg设置为错误消息。当errmsg非NULL时,可以在不持有Python GIL的情况下安全地调用此函数。

char **NpyIter_GetDataPtrArray(NpyIter *iter)#

此函数返回指向`nop`数据指针的指针。如果没有指定NPY_ITER_EXTERNAL_LOOP,则每个数据指针都指向迭代器的当前数据项。如果没有指定内部迭代,则它指向内部循环的第一个数据项。

此指针可以在迭代循环之前缓存,调用iternext不会改变它。此函数可以在不持有 Python GIL 的情况下安全调用。

char **NpyIter_GetInitialDataPtrArray(NpyIter *iter)#

获取直接指向数组(而不是缓冲区)的数据指针数组,对应于迭代索引 0。

这些指针与NpyIter_ResetBasePointers接受的指针不同,因为沿某些轴的方向可能已被反转。

此函数可以在不持有 Python GIL 的情况下安全调用。

npy_intp *NpyIter_GetIndexPtr(NpyIter *iter)#

此函数返回指向正在跟踪的索引的指针,如果未跟踪任何索引,则返回 NULL。只有在构造过程中指定了标志NPY_ITER_C_INDEXNPY_ITER_F_INDEX之一时才能使用。

当使用标志NPY_ITER_EXTERNAL_LOOP时,代码需要知道执行内部循环的参数。这些函数提供这些信息。

npy_intp *NpyIter_GetInnerStrideArray(NpyIter *iter)#

返回一个指向`nop`步长的数组的指针,每个迭代对象一个,供内部循环使用。

此指针可以在迭代循环之前缓存,调用iternext不会改变它。此函数可以在不持有 Python GIL 的情况下安全调用。

**警告:**虽然指针可以缓存,但如果迭代器被缓冲,其值可能会发生变化。

npy_intp *NpyIter_GetInnerLoopSizePtr(NpyIter *iter)#

返回一个指向内部循环应执行的迭代次数的指针。

此地址可以在迭代循环之前缓存,调用iternext不会改变它。值本身在迭代过程中可能会改变,特别是如果启用了缓冲。此函数可以在不持有 Python GIL 的情况下安全调用。

void NpyIter_GetInnerFixedStrideArray(NpyIter *iter, npy_intp *out_strides)#

获取一个步长数组,这些步长是固定的,或者在整个迭代过程中不会改变。对于可能改变的步长,步长中将放置值 NPY_MAX_INTP。

一旦迭代器准备好迭代(如果使用了NPY_ITER_DELAY_BUFALLOC,则在重置之后),调用此函数以获取可用于选择快速内部循环函数的步长。例如,如果步长为 0,则意味着内部循环可以始终一次将它的值加载到变量中,然后在整个循环中使用该变量;或者如果步长等于 itemsize,则可以使用该操作数的连续版本。

此函数可以在不持有 Python GIL 的情况下安全调用。

从以前的 NumPy 迭代器转换#

旧的迭代器 API 包括诸如 PyArrayIter_Check、PyArray_Iter* 和 PyArray_ITER_* 之类的函数。多迭代器数组包括 PyArray_MultiIter*、PyArray_Broadcast 和 PyArray_RemoveSmallest。新的迭代器设计用单个对象和相关的 API 替换了所有这些功能。新 API 的一个目标是所有现有迭代器的用法都可以在不付出太大努力的情况下用新的迭代器替换。在 1.6 版本中,主要的例外是邻域迭代器,此迭代器中没有对应的功能。

这是一个转换表,说明了在新迭代器中使用哪些函数