NumPy-1-26-中文文档-二十一-
NumPy 1.26 中文文档(二十一)
数组迭代器 API
在 1.6 版中新增。
数组迭代器
数组迭代器封装了通用函数中的许多关键功能,允许用户代码支持输出参数、保留内存布局和使用错误对齐或类型的数据缓冲,而无需进行困难的编码。
本页记录了迭代器的 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_New( *op, flags, order, casting, *dtype)
为给定的 numpy 数组对象op
创建一个迭代器。
在flags
中可能传递的标志是NpyIter_MultiNew
中记录的全局和每个操作数标志的任意组合,除了NPY_ITER_ALLOCATE
。
可以将任何NPY_ORDER
枚举值传递给order
。为了进行高效的迭代,NPY_KEEPORDER
是最佳选项,其他顺序则需要特定的迭代模式。
可以将任何NPY_CASTING
枚举值传递给casting
。这些值包括NPY_NO_CASTING
、NPY_EQUIV_CASTING
、NPY_SAFE_CASTING
、NPY_SAME_KIND_CASTING
和NPY_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_MultiNew( nop, **op, flags, order, casting, *op_flags, **op_dtypes)
创建用于对提供给op
的nop
数组对象进行广播的迭代器,使用常规的 NumPy 广播规则。
可以将任何NPY_ORDER
枚举值传递给order
。为了进行高效的迭代,NPY_KEEPORDER
是最佳选项,其他顺序则需要特定的迭代模式。在使用NPY_KEEPORDER
时,如果你还希望确保迭代沿轴不发生反转,则应该传递标志NPY_ITER_DONT_NEGATE_STRIDES
。
可以将任何NPY_CASTING
枚举值传递给casting
。这些值包括NPY_NO_CASTING
、NPY_EQUIV_CASTING
、NPY_SAFE_CASTING
、NPY_SAME_KIND_CASTING
和NPY_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_INDEX
、NPY_ITER_F_INDEX
和 NPY_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_BUFFERED
时,此标志才能与NPY_ITER_EXTERNAL_LOOP
一起使用。这是因为没有缓冲处理时,内部循环总是最内部迭代维度的大小,并且允许它被切割需要特殊处理,实际上更像是缓冲版本。
NPY_ITER_BUFFERED
使迭代器存储缓冲数据,并使用缓冲来满足数据类型、对齐和字节顺序要求。要对操作数进行缓冲处理,不要指定NPY_ITER_COPY
或NPY_ITER_UPDATEIFCOPY
标志,因为它们会覆盖缓冲处理。缓冲对于使用迭代器的 Python 代码特别有用,允许一次处理更大块的数据,以分摊 Python 解释器的开销。
如果与NPY_ITER_EXTERNAL_LOOP
一起使用,调用者的内部循环可能会获得比没有缓冲更大的块,这是因为步幅的布局方式的原因。
请注意,如果一个操作数被赋予标志NPY_ITER_COPY
或NPY_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
如果任何写操作数与任何读操作数存在重叠,通过制作临时副本(必要时启用 WRITEIFCOPY 以避免重叠),消除所有重叠。如果有一个内存地址包含两个数组的共同数据,则一对操作数具有重叠。
由于精确的重叠检测在维度数量上具有指数级的运行时间,因此决策是基于启发式方法的,该方法具有假阳性(在不寻常的情况下产生不必要的副本),但没有假阴性。
如果存在任何读/写重叠,此标志可确保操作的结果与所有操作数进行复制时的结果相同。在需要进行复制的情况下,如果没有此标志,计算结果可能是不确定的!
可以传递到op_flags[i]
的标志,其中0 <= i < nop
:
NPY_ITER_READWRITE
NPY_ITER_READONLY
NPY_ITER_WRITEONLY
指示迭代器的使用者将如何对op[i]
进行读取或写入。对于每个操作数,必须指定这些标志中的一个。对于用户提供的操作数,使用NPY_ITER_READWRITE
或NPY_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_WRITEONLY
或NPY_ITER_READWRITE
。如果 op[i]
为 NULL,则创建一个具有最终广播维度和与迭代器的迭代顺序匹配的布局的新数组。
当 op[i]
为 NULL 时,请求的数据类型 op_dtypes[i]
也可以为 NULL,此时它将自动从标记为可读的数组的数据类型中生成。生成数据类型的规则与 UFuncs 相同。特别注意的是选择的数据类型中的字节顺序处理。如果只有一个输入,则使用输入的数据类型。否则,如果将多个输入数据类型组合在一起,则输出将为本机字节顺序。
带有此标志分配后,调用者可以通过调用NpyIter_GetOperandArray
来检索新的数组,并获取返回的 C 数组中的第 i 个对象。调用者必须调用 Py_INCREF 来声明对数组的引用。
NPY_ITER_NO_SUBTYPE
用于NPY_ITER_ALLOCATE
时,此标志禁用为输出分配数组子类型,强制其成为直接的 ndarray。
TODO: 或许引入一个函数 NpyIter_GetWrappedOutput
并删除此标志会更好?
NPY_ITER_NO_BROADCAST
确保输入或输出与迭代维度完全匹配。
NPY_ITER_ARRAYMASK
1.7 版本中的新功能。
表示这个操作数是在写入操作数时要使用的掩码。当应用了NPY_ITER_WRITEMASKED
标志时,只能有一个操作数应用了NPY_ITER_ARRAYMASK
标志。
具有此标志的操作数的数据类型应为NPY_BOOL
、NPY_MASK
或所有字段都是有效掩码数据类型的结构 dtype。在后一种情况下,它必须与 WRITEMASKED 的结构操作数匹配,因为它在指定该数组的每个字段的掩码。
此标志仅影响从缓冲区写回数组。这意味着如果操作数还是NPY_ITER_READWRITE
或NPY_ITER_WRITEONLY
,执行迭代的代码可以写入此操作数以控制哪些元素将不被修改,哪些元素将被修改。当掩码应为输入掩码的组合时,这很有用。
NPY_ITER_WRITEMASKED
从版本 1.7 开始新增。
此数组是所有writemasked
操作数的掩码。代码使用writemasked
标志,指示只有选择的 ARRAYMASK 操作数为 True 的元素才会被写入。一般情况下,迭代器不会强制执行此操作,代码执行迭代时应遵循该约定。
当使用writemasked
标志并且此操作数已缓冲时,这会改变从缓冲区复制数据到数组的方式。将使用掩码复制例程,该例程仅复制在数组掩码的相应元素中writemasked
返回 true 的元素。
NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE
在内存重叠检查中,假设启用了NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE
的操作数仅按照迭代器顺序访问。
这使得迭代器能够推断数据依赖关系,可能避免不必要的复制。
此标志仅在迭代器上启用了NPY_ITER_COPY_IF_OVERLAP
时才有效。
*NpyIter_AdvancedNew( nop, **op, flags, order, casting, *op_flags, **op_dtypes, int oa_ndim, int **op_axes, const *itershape, buffersize)
使用几个高级选项扩展了NpyIter_MultiNew
,提供了对广播和缓冲的更多控制。
如果将-1/NULL 值传递给oa_ndim
、op_axes
、itershape
和buffersize
,则等效于NpyIter_MultiNew
。
当参数oa_ndim
不为零或-1 时,指定将使用自定义广播进行迭代的维数的数量。如果提供了它,op_axes
必须提供,并且itershape
也可以提供。op_axes
参数让您可以详细控制操作数组的轴如何匹配在一起并进行迭代。在op_axes
中,您必须提供一个指向大小为oa_ndim
的数组的指针数组,其类型为npy_intp
。如果op_axes
中的条目为 NULL,则将应用正常的广播规则。在op_axes[j][i]
中存储的是op[j]
的一个有效轴,或者是-1,表示newaxis
。在每个op_axes[j]
数组内,轴不得重复。以下示例是如何将正常的广播应用于三维数组、二维数组、一维数组和标量的。
注意:在 NumPy 1.8 之前,oa_ndim == 0
用于表示op_axes
和itershape
未使用。这已被弃用,应替换为-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
是零,则使用默认的缓冲区大小,否则指定要使用多大的缓冲区。建议使用 4096 或 8192 等的 2 的幂大小的缓冲区。
如果出现错误,返回 NULL,否则返回分配的迭代器。
*NpyIter_Copy( *iter)
复制给定迭代器。此函数主要用于启用数据的多线程迭代。
TODO:将此移到有关多线程迭代的部分。
推荐的多线程迭代方法是首先使用标志 NPY_ITER_EXTERNAL_LOOP
、NPY_ITER_RANGED
、NPY_ITER_BUFFERED
、NPY_ITER_DELAY_BUFALLOC
和可能的 NPY_ITER_GROWINNER
创建一个迭代器。为每个线程创建此迭代器的副本(第一个迭代器除外)。然后,将迭代索引范围 0, NpyIter_GetIterSize(iter))
分成任务进行处理,例如使用 TBB 的 parallel_for 循环。当线程获得任务并执行时,它将使用自己的迭代器副本,通过调用 [NpyIter_ResetToIterIndexRange
并对整个范围进行迭代。
在多线程代码中使用迭代器或在不持有 Python GIL 的代码中使用时,必须注意只调用在该上下文中安全的函数。不能在没有 Python GIL 的情况下安全调用 NpyIter_Copy
,因为它会增加 Python 引用。通过将 errmsg
参数传递为非 NULL,可以安全调用 Reset*
和一些其他函数,这样函数将通过它传回错误,而不是设置 Python 异常。
每个副本都必须调用 NpyIter_Deallocate
。
int NpyIter_RemoveAxis( *iter, int axis)
从迭代中移除一个轴。这要求在迭代器创建时设置了 NPY_ITER_MULTI_INDEX
,并且如果启用了缓冲或正在跟踪索引,则不起作用。此函数还将迭代器重置到初始状态。
这对于设置累加循环非常有用。迭代器可以首先使用包括累加轴在内的所有维度创建,以便输出正确创建。然后,累加轴可以被移除,并且计算以嵌套的方式进行。
警告:此函数可能会改变迭代器的内部内存布局。必须重新获取迭代器的任何缓存函数或指针!迭代范围也将被重置。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_RemoveMultiIndex( *iter)
如果迭代器正在跟踪多索引,则取消对它们的支持,并进行进一步的迭代器优化,如果不需要多索引。此函数也会将迭代器重置回初始状态。
警告:此函数可能会改变迭代器的内部内存布局。必须重新获取迭代器的任何缓存函数或指针!
调用此函数后,NpyIter_HasMultiIndex(iter) 将返回 false。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_EnableExternalLoop( *iter)
如果调用了 NpyIter_RemoveMultiIndex
,可能需要启用标志 NPY_ITER_EXTERNAL_LOOP
。此标志与 NPY_ITER_MULTI_INDEX
不允许同时存在,因此提供了这个函数在调用 NpyIter_RemoveMultiIndex
后启用该功能。此函数也会将迭代器重置回初始状态。
警告:此函数更改了迭代器的内部逻辑。必须重新获取迭代器的任何缓存函数或指针!
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_Deallocate( *iter)
释放迭代器对象并解决任何需要的写回。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_Reset( *iter, char **errmsg)
将迭代器重置为初始状态,在迭代范围的起始位置。
返回 NPY_SUCCEED
或 NPY_FAIL
。如果 errmsg 非空,则在返回NPY_FAIL
时不会设置 Python 异常。相反,*errmsg 被设置为错误消息。当 errmsg 非空时,可以安全地调用该函数而不持有 Python GIL。
int NpyIter_ResetToIterIndexRange( *iter, istart, 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_SUCCEED
或 NPY_FAIL
。如果 errmsg 非空,则在返回NPY_FAIL
时不会设置 Python 异常。相反,*errmsg 被设置为错误消息。当 errmsg 非空时,可以安全地调用该函数而不持有 Python GIL。
int NpyIter_ResetBasePointers( *iter, char **baseptrs, char **errmsg)
将迭代器重置回初始状态,但使用baseptrs
中的值作为数据而不是从正在迭代的数组的指针中获取。此函数预期与op_axes
参数一起由具有两个或多个迭代器的嵌套迭代代码一起使用。
返回NPY_SUCCEED
或NPY_FAIL
。如果errmsg
非空,则在返回NPY_FAIL
时不设置 Python 异常。相反,errmsg将设置为错误消息。当errmsg
非空时,可以安全地调用该函数而不持有 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( *iter, const *multi_index)
调整迭代器以指向由multi_index
指向的ndim
索引。如果没有跟踪多索引、索引越界或禁用内部循环迭代,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
int NpyIter_GotoIndex( *iter, index)
调整迭代器以指向指定的index
。如果迭代器是用标志NPY_ITER_C_INDEX
构造的,则index
是 C 顺序索引;如果迭代器是用标志NPY_ITER_F_INDEX
构造的,则index
是 Fortran 顺序索引。如果没有正在跟踪索引、索引越界或禁用内部循环迭代,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_GetIterSize( *iter)
返回正在迭代的元素数量。这是形状中所有维度的乘积。当跟踪多索引(并且可能调用NpyIter_RemoveAxis)时,大小可能为-1
,表示迭代器太大。这样的迭代器是无效的,但在调用NpyIter_RemoveAxis后可能变为有效。不需要检查这种情况。
NpyIter_GetIterIndex( *iter)
获取迭代器的iterindex
,这是与迭代器的迭代顺序匹配的索引。
void NpyIter_GetIterIndexRange( *iter, *istart, *iend)
获取正在进行迭代的iterindex
子范围。如果未指定NPY_ITER_RANGED
,则始终返回范围0, NpyIter_IterSize(iter))
。
int NpyIter_GotoIterIndex( *iter, iterindex)
调整迭代器以指向指定的iterindex
。如果iterindex
越界,启用了缓冲区或禁用内部循环迭代,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_HasDelayedBufAlloc( *iter)
如果向迭代器构造器传递了标志[NPY_ITER_DELAY_BUFALLOC
,并且尚未调用 Reset 函数之一,返回 1,否则返回 0。
NpyIter_HasExternalLoop( *iter)
如果调用者需要处理最内层的 1 维循环,则返回 1,如果迭代器处理所有循环,则返回 0。这由构造函数标志NPY_ITER_EXTERNAL_LOOP
或NpyIter_EnableExternalLoop
控制。
NpyIter_HasMultiIndex( *iter)
如果迭代器是用NPY_ITER_MULTI_INDEX
标志创建的,则返回 1,否则返回 0。
NpyIter_HasIndex( *iter)
如果迭代器是用NPY_ITER_C_INDEX
或NPY_ITER_F_INDEX
标志创建的,则返回 1,否则返回 0。
NpyIter_RequiresBuffering( *iter)
如果迭代器需要缓冲,则返回 1,这发生在操作数需要转换或对齐,因此无法直接使用。
NpyIter_IsBuffered( *iter)
如果迭代器是用NPY_ITER_BUFFERED
标志创建的,则返回 1,否则返回 0。
NpyIter_IsGrowInner( *iter)
如果迭代器是用NPY_ITER_GROWINNER
标志创建的,则返回 1,否则返回 0。
NpyIter_GetBufferSize( *iter)
如果迭代器被缓冲,则返回正在使用的缓冲区的大小,否则返回 0。
int NpyIter_GetNDim( *iter)
返回正在进行迭代的维度数。如果在迭代器构造函数中未请求多索引,则此值可能小于原始对象中的维度数。
int NpyIter_GetNOp( *iter)
返回迭代器中的操作数数量。
*NpyIter_GetAxisStrideArray( *iter, int axis)
获取指定轴的步幅数组。要求迭代器跟踪多索引,并且未启用缓冲区。
当您想以某种方式匹配操作数轴然后使用NpyIter_RemoveAxis
手动处理它们时,可以使用此功能。在删除轴之前调用此函数,可以获取手动处理的步幅。
出现错误时返回NULL
。
int NpyIter_GetShape( *iter, *outshape)
返回outshape
中迭代器的广播形状。这只能在正在跟踪多索引的迭代器上调用。
返回NPY_SUCCEED
或NPY_FAIL
。
**NpyIter_GetDescrArray( *iter)
这将返回指向正在迭代的对象的nop
数据类型 Descrs 的指针。结果指向iter
,所以调用者不会获得对 Descrs 的任何引用。
在迭代循环之前可以缓存此指针,调用iternext
不会改变它。
**NpyIter_GetOperandArray( *iter)
这将返回指向正在迭代的nop
操作PyObjects
的指针。结果指向iter
,所以调用者不会获得对PyObjects
的任何引用。
*NpyIter_GetIterView( *iter, i)
这将返回对新的 ndarray 视图的引用,该视图是一个对数组NpyIter_GetOperandArray
中第 i 个对象的视图,其维度和步幅与内部优化的迭代模式匹配。对此视图的 C 顺序迭代等同于迭代器的迭代顺序。
例如,如果使用单个数组作为输入创建了迭代器,并且可以重排所有轴,然后将其折叠为单个分段迭代,那么这将返回一个视图,即一个一维数组。
void NpyIter_GetReadFlags( *iter, char *outreadflags)
填充nop
标志。如果可以从op[i]
读取,则将outreadflags[i]
设置为 1,否则设置为 0。
void NpyIter_GetWriteFlags( *iter, char *outwriteflags)
填充nop
标志。如果可以写入op[i]
,则将outwriteflags[i]
设置为 1,否则设置为 0。
int NpyIter_CreateCompatibleStrides( *iter, itemsize, *outstrides)
构建的步幅集(strides)与使用NPY_ITER_ALLOCATE
标志创建的输出数组的步幅相同,其中对于op_axes
传递的是 NULL。这适用于数据按紧密方式打包,但未必按照 C 或 Fortran 顺序。这应与传入构造函数的标志NPY_ITER_MULTI_INDEX
一起使用NpyIter_GetShape
和NpyIter_GetNDim
。
此函数的用例是匹配迭代器的形状和布局,并添加一个或多个维度。例如,为了为数值梯度生成每个输入值的向量,您传入ndim*itemsize
用于itemsize
,然后在末尾添加一个大小为ndim
且步幅为itemsize
的维度。要进行 Hessian 矩阵操作,您可以做同样的事情,但添加两个维度,或者利用对称性并使用特定编码将其打包为 1 个维度。
只有在迭代器跟踪多索引且使用NPY_ITER_DONT_NEGATE_STRIDES
防止轴按相反顺序进行迭代时,才可以调用此函数。
如果使用此方法创建数组,则每次迭代只需为itemsize
添加即可遍历新数组与迭代器匹配。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_IsFirstVisit( *iter, int iop)
版本 1.7 中的新增内容。
检查指定的缩减操作数的元素是否是第一次被迭代器指向的。该函数对于缩减操作数和禁用缓冲的情况下返回合理的答案。对于带有缓冲区的非缩减操作数,答案可能不正确。
此函数仅适用于 EXTERNAL_LOOP 模式,并且在未启用该模式时将产生一些错误答案。
如果此函数返回 true,则调用者还应检查操作数的内部循环跨度,因为如果该跨度为 0,则只有第一个元素的内部最外部循环正在首次访问。
警告:出于性能原因,‘iop’没有经过边界检查,没有确认‘iop’实际上是一个减少的操作数,并且没有确认已启用 EXTERNAL_LOOP 模式。这些检查是调用者的责任,并应在任何内部循环之外进行。
用于迭代的函数
*NpyIter_GetIterNext( *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_BUFFERED
和 NPY_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_GetGetMultiIndex( *iter, char **errmsg)
返回一个函数指针以获取迭代器的当前多索引。如果迭代器没有跟踪多索引,则返回 NULL。建议在迭代循环之前将该函数指针缓存到本地变量中。
如果发生错误,则返回 NULL。如果 errmsg 非 NULL,则在返回NPY_FAIL
时不会设置 Python 异常。相反,*errmsg 会设置为错误消息。当 errmsg 非 NULL 时,可以在不持有 Python GIL 的情况下安全调用该函数。
char **NpyIter_GetDataPtrArray( *iter)
这会返回指向nop
数据指针的指针。如果未指定NPY_ITER_EXTERNAL_LOOP
,则每个数据指针指向迭代器的当前数据项。如果未指定内部迭代,则指向内部循环的第一个数据项。
此指针可能在迭代循环之前被缓存,调用iternext
不会改变它。此函数可以在不持有 Python 全局解释器锁(GIL)的情况下安全地调用。
char **NpyIter_GetInitialDataPtrArray( *iter)
直接将数据指针数组放入数组中(而不是放入缓冲区),对应迭代索引 0。
这些指针与NpyIter_ResetBasePointers
接受的指针不同,因为沿某些轴的方向可能已经被颠倒。
此函数在不持有 Python 全局解释器锁(GIL)的情况下可以安全地调用。
*NpyIter_GetIndexPtr( *iter)
这会返回指向正在跟踪的索引的指针,如果没有正在跟踪的索引,则返回 NULL。只有在构建过程中指定了标志NPY_ITER_C_INDEX
或NPY_ITER_F_INDEX
之一时才能使用它。
当使用标志NPY_ITER_EXTERNAL_LOOP
时,代码需要知道执行内部循环的参数。这些函数提供了这些信息。
*NpyIter_GetInnerStrideArray( *iter)
返回一个指向nop
步幅数组的指针,每个迭代对象使用一个步幅,以供内部循环使用。
此指针可能在迭代循环之前被缓存,调用iternext
不会改变它。此函数可以在不持有 Python 全局解释器锁(GIL)的情况下安全地调用。
警告:虽然指针可能被缓存,但如果迭代器被缓冲,其值可能会发生变化。
*NpyIter_GetInnerLoopSizePtr( *iter)
返回一个指向内部循环应执行的迭代次数的指针。
此地址可能在迭代循环之前被缓存,调用iternext
不会改变它。值本身在迭代期间可能会发生变化,特别是如果启用了缓冲区。此函数可以在不持有 Python 全局解释器锁(GIL)的情况下安全地调用。
void NpyIter_GetInnerFixedStrideArray( *iter, *out_strides)
获取一组固定的或在整个迭代过程中不会改变的步幅。对于可能发生变化的步幅,位置NPY_MAX_INTP
的值将被放置在步幅中。
一旦迭代器准备好进行迭代(如果使用了NPY_ITER_DELAY_BUFALLOC
,则在重置后),调用此函数以获取可用于选择快速内循环函数的步长。例如,如果步幅为 0,这意味着内部循环总是可以将其值加载到变量中一次,然后在整个循环中使用变量,或者如果步幅等于项大小,则可以使用该操作数的连续版本。
此函数在不持有 Python 全局解释器锁(GIL)的情况下可以安全地调用。
从以前的 NumPy 迭代器转换
旧的迭代器 API 包括 PyArrayIter_Check、PyArray_Iter* 和 PyArray_ITER_* 等函数。多迭代器数组包括 PyArray_MultiIter*、PyArray_Broadcast 和 PyArray_RemoveSmallest。新的迭代器设计用单个对象和关联的 API 替换了所有这些功能。新 API 的一个目标是,现有迭代器的所有用法都应该能够毫不费力地替换为新迭代器。在 1.6 版本中,唯一的例外是邻域迭代器,在这个迭代器中没有对应的功能。
以下是与新迭代器一起使用的函数的转换表:
Iterator Functions | |
---|---|
PyArray_IterNew |
NpyIter_New |
PyArray_IterAllButAxis |
NpyIter_New + axes 参数 或 迭代器标志 NPY_ITER_EXTERNAL_LOOP |
PyArray_BroadcastToShape |
NOT SUPPORTED (Use the support for multiple operands instead.) |
PyArrayIter_Check |
将需要在 Python 暴露中添加此功能 |
PyArray_ITER_RESET |
NpyIter_Reset |
PyArray_ITER_NEXT |
NpyIter_GetIterNext 中的函数指针 |
PyArray_ITER_DATA |
NpyIter_GetDataPtrArray |
PyArray_ITER_GOTO |
NpyIter_GotoMultiIndex |
PyArray_ITER_GOTO1D |
NpyIter_GotoIndex 或 NpyIter_GotoIterIndex |
PyArray_ITER_NOTDONE |
iternext 函数指针的返回值 |
多迭代器功能 | |
PyArray_MultiIterNew |
NpyIter_MultiNew |
PyArray_MultiIter_RESET |
NpyIter_Reset |
PyArray_MultiIter_NEXT |
NpyIter_GetIterNext 中的函数指针 |
PyArray_MultiIter_DATA |
NpyIter_GetDataPtrArray |
PyArray_MultiIter_NEXTi |
不支持(始终为锁定步进迭代) |
PyArray_MultiIter_GOTO |
NpyIter_GotoMultiIndex |
PyArray_MultiIter_GOTO1D |
NpyIter_GotoIndex 或NpyIter_GotoIterIndex |
PyArray_MultiIter_NOTDONE |
iternext 函数指针的返回值 |
PyArray_Broadcast |
由NpyIter_MultiNew 处理 |
PyArray_RemoveSmallest |
迭代器标志 NPY_ITER_EXTERNAL_LOOP |
其他功能 | |
PyArray_ConvertToCommonType |
迭代器标志 NPY_ITER_COMMON_DTYPE |
数组迭代器
数组迭代器封装了 ufuncs 中许多关键功能,允许用户代码支持功能,如输出参数、保留内存布局和缓冲具有错误对齐或类型的数据,而无需编写困难的编码。
该页面记录了迭代器的 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_New( *op, flags, order, casting, *dtype)
为给定的 numpy 数组对象op
创建一个迭代器。
可以在flags
中传递的标志是NpyIter_MultiNew
中记录的全局和每个操作数标志的任意组合,除了NPY_ITER_ALLOCATE
。
任何NPY_ORDER
枚举值都可以传递给order
。为了进行高效迭代,NPY_KEEPORDER
是最佳选项,其他顺序强制执行特定的迭代模式。
任何NPY_CASTING
枚举值都可以传递给casting
。这些值包括NPY_NO_CASTING
、NPY_EQUIV_CASTING
、NPY_SAFE_CASTING
、NPY_SAME_KIND_CASTING
和NPY_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_MultiNew( nop, **op, flags, order, casting, *op_flags, **op_dtypes)
创建一个迭代器,用于广播op
中提供的nop
数组对象,使用常规的 NumPy 广播规则。
order
参数可以传递NPY_ORDER
枚举值中的任意一个。为了有效迭代,NPY_KEEPORDER
是最佳选项,其他顺序强制执行特定的迭代模式。当使用NPY_KEEPORDER
时,如果您还希望确保迭代沿某个轴不被翻转,应该传递标志NPY_ITER_DONT_NEGATE_STRIDES
。
casting
参数可以传递NPY_CASTING
枚举值中的任意一个。这些值包括NPY_NO_CASTING
、NPY_EQUIV_CASTING
、NPY_SAFE_CASTING
、NPY_SAME_KIND_CASTING
和NPY_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_INDEX
、NPY_ITER_F_INDEX
和NPY_ITER_MULTI_INDEX
不兼容。
NPY_ITER_DONT_NEGATE_STRIDES
当指定顺序参数为NPY_KEEPORDER
时,这仅影响迭代器。默认情况下,使用NPY_KEEPORDER
时,迭代器颠倒具有负步幅的轴,以便内存以正向顺序遍历。这会禁用此步骤。如果要使用轴的底层内存顺序,但不希望反转轴,则使用此标志。例如,numpy.ravel(a, order='K')
的行为。
NPY_ITER_COMMON_DTYPE
使迭代器将所有操作数转换为一个共同的数据类型,根据 ufunc 类型提升规则计算。必须启用复制或缓冲。
如果已经提前知道了共同的数据类型,请不要使用此标志。而是为所有操作数设置请求的 dtype。
NPY_ITER_REFS_OK
表示接受并在迭代器中使用具有引用类型(对象数组或包含对象类型的结构化数组)的数组。如果启用了此标志,则调用方必须确保检查是否在迭代期间需要 API(iter)为真,这种情况下可能不会在迭代期间释放 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_BUFFERED
时,此标志只能与NPY_ITER_EXTERNAL_LOOP
一起使用。这是因为如果没有缓冲,内部循环总是内层迭代维度的大小,并且允许它被分割将需要特殊处理,实际上使其更像缓冲版本。
NPY_ITER_BUFFERED
导致迭代器存储缓冲区数据,并使用缓冲来满足数据类型、对齐和字节顺序要求。 要缓冲操作数,请不要指定NPY_ITER_COPY
或NPY_ITER_UPDATEIFCOPY
标志,因为它们会覆盖缓冲。 对于使用迭代器的 Python 代码,缓冲特别有用,允许一次处理更大的数据块以摊销 Python 解释器的开销。
如果与NPY_ITER_EXTERNAL_LOOP
一起使用,则调用者的内部循环可能会比没有缓冲区时得到更大的块,这是因为步幅的布局方式。
请注意,如果给一个操作数设置了标志NPY_ITER_COPY
或NPY_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_READWRITE
或NPY_ITER_WRITEONLY
用于用户提供的操作数可能触发WRITEBACKIFCOPY语义。在调用 NpyIter_Deallocate
时,数据将写回原始数组。
NPY_ITER_COPY
如果 op[i]
不符合构造函数标志和参数指定的数据类型或对齐要求,则允许对 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_WRITEONLY
或NPY_ITER_READWRITE
。如果 op[i]
为 NULL,则创建一个具有最终广播维度和与迭代器的迭代顺序相匹配的布局的新数组。
当 op[i]
为 NULL 时,所请求的数据类型op_dtypes[i]
也可能为 NULL,这种情况下会自动生成它,其来自被标记为可读取的数组的数据类型。生成数据类型的规则与 UFuncs 相同。特别需要注意的是所选数据类型的字节序处理方法。如果只有一个输入,将直接使用输入的数据类型。否则,如果将多个输入数据类型组合在一起,则输出将采用本机字节序。
使用此标志分配后,调用者可以通过调用NpyIter_GetOperandArray
并获取返回的 C 数组中的第 i 个对象来检索新数组。调用者必须调用 Py_INCREF 来声明对数组的引用。
NPY_ITER_NO_SUBTYPE
用于NPY_ITER_ALLOCATE
,此标志禁用为输出分配数组子类型,强制其为一维 ndarray。
TODO:也许引入一个函数NpyIter_GetWrappedOutput
并删除此标志会更好?
NPY_ITER_NO_BROADCAST
确保输入或输出与迭代维度完全匹配。
NPY_ITER_ARRAYMASK
自版本 1.7 开始。
指示这个操作数是用于在写入应用了NPY_ITER_WRITEMASKED
标志的操作数时使用的掩码。只有一个操作数可以应用NPY_ITER_ARRAYMASK
标志。
具有此标志的操作数的数据类型应为NPY_BOOL
,NPY_MASK
或其字段都是有效掩码数据类型的结构数据类型。在后一种情况下,它必须与被 WRITEMASKED 的结构操作数匹配,因为它指定了该数组每个字段的掩码。
此标记仅影响从缓冲区回写到数组。这意味着,如果操作数还是NPY_ITER_READWRITE
或NPY_ITER_WRITEONLY
,则进行迭代的代码可以写入此操作数以控制哪些元素将不受影响,哪些将被修改。当掩码应该是输入掩码的组合时,这是有用的。
NPY_ITER_WRITEMASKED
自版本 1.7 开始。
此数组是所有writemasked
操作数的掩码。代码使用writemasked
标志,指示只会写入选择的 ARRAYMASK 操作数为 True 的元素。一般来说,迭代器并不强制这一点,迭代的代码应该遵循这个承诺。
当使用writemasked
标志并且此操作数被缓冲时,这会改变数据从缓冲区复制到数组的方式。会使用一个带掩码的复制例程,仅复制缓冲区中writemasked
从相应的 ARRAYMASK 操作数返回 True 的元素。
NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE
在内存重叠检查中,假设启用了NPY_ITER_OVERLAP_ASSUME_ELEMENTWISE
的操作数只能按照迭代器顺序访问。
这使得迭代器能够推理数据依赖性,可能避免不必要的复制。
仅当迭代器上启用了NPY_ITER_COPY_IF_OVERLAP
时,此标志生效。
*NpyIter_AdvancedNew( nop, **op, flags, order, casting, *op_flags, **op_dtypes, int oa_ndim, int **op_axes, const *itershape, buffersize)
通过提供几个高级选项,扩展了NpyIter_MultiNew
,提供了更多对广播和缓冲的控制。
如果将-1/NULL 值传递给oa_ndim
、op_axes
、itershape
和buffersize
,则相当于NpyIter_MultiNew
。
当参数oa_ndim
不为零或-1 时,指定将使用定制广播迭代的维度数量。如果提供了op_axes
,则必须提供itershape
。op_axes
参数允许您详细控制操作数数组的轴如何匹配在一起并进行迭代。在op_axes
中,必须提供nop
指针数组,指向大小为oa_ndim
的npy_intp
类型数组。如果op_axes
中的条目为 NULL,则将应用正常的广播规则。op_axes[j][i]
中存储了op[j]
的有效轴,或者是-1 表示newaxis
。在每个op_axes[j]
数组中,轴不能重复。以下示例是正常广播应用到 3D 数组、2D 数组、1D 数组和标量的情况。
注意:在 NumPy 1.8 之前,oa_ndim == 0
用于表示``op_axes和
itershape未使用。这已经不推荐使用,应替换为-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_Copy( *iter)
复制给定迭代器。此函数主要用于使数据多线程迭代。
TODO:将此内容移到关于多线程迭代的部分。
多线程迭代的推荐方法是首先使用标记NPY_ITER_EXTERNAL_LOOP
、NPY_ITER_RANGED
、NPY_ITER_BUFFERED
、NPY_ITER_DELAY_BUFALLOC
,可能还有NPY_ITER_GROWINNER
创建一个迭代器。为每个线程创建此迭代器的副本(第一个迭代器减去一)。然后,将迭代索引范围0,NpyIter_GetIterSize(iter))
分割成任务,例如使用 TBB parallel_for 循环。当一个线程获得要执行的任务时,它将使用其迭代器的副本,通过调用[NpyIter_ResetToIterIndexRange
并迭代整个范围。
在多线程代码或不持有 Python GIL 的代码中使用迭代器时,必须小心地只调用在该上下文中安全的函数。不能在没有 Python GIL 的情况下安全调用 NpyIter_Copy
,因为它会增加 Python 引用。Reset*
和一些其他函数可以安全调用,通过传递非 NULL 的 errmsg
参数,这样函数将通过它传回错误,而不是设置 Python 异常。
必须为每个副本调用 NpyIter_Deallocate
。
int NpyIter_RemoveAxis( *iter, int axis)
从迭代中移除一个轴。这要求在迭代器创建时设置了 NPY_ITER_MULTI_INDEX
,并且在启用缓冲或正在跟踪索引时无法工作。此函数还将迭代器重置回初始状态。
例如用于设置累积循环很有用。迭代器可以首先使用所有维度创建,包括累积轴,以便正确创建输出。然后,可以移除累积轴,并采用嵌套的方式进行计算。
警告:此函数可能会更改迭代器的内部内存布局。必须重新获取迭代器的任何缓存功能或指针!迭代范围也将被重置。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_RemoveMultiIndex( *iter)
如果迭代器正在跟踪多重索引,则会取消对它们的支持,并对迭代器进行可能的进一步优化,如果不需要多重索引的话。此函数还将迭代器重置回初始状态。
警告:此函数可能会更改迭代器的内部内存布局。必须重新获取迭代器的任何缓存功能或指针!
调用此函数后,NpyIter_HasMultiIndex(iter) 将返回 false。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_EnableExternalLoop( *iter)
如果调用了 NpyIter_RemoveMultiIndex
,您可能希望启用标志 NPY_ITER_EXTERNAL_LOOP
。这个标志与 NPY_ITER_MULTI_INDEX
不允许一起使用,因此在调用了 NpyIter_RemoveMultiIndex
之后提供此函数以启用该功能。此函数还将迭代器重置回初始状态。
警告:此函数更改了迭代器的内部逻辑。必须重新获取迭代器的任何缓存功能或指针!
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_Deallocate( *iter)
释放迭代器对象并解决任何需要的写回。
返回 NPY_SUCCEED
或 NPY_FAIL
。
int NpyIter_Reset( *iter, char **errmsg)
将迭代器重置回初始状态,即迭代范围的开始处。
返回NPY_SUCCEED
或NPY_FAIL
。如果 errmsg
非空,则当返回NPY_FAIL
时不会设置任何 Python 异常。相反,errmsg 会被设置为错误消息。当 errmsg
非空时,可以在不持有 Python GIL 的情况下安全地调用该函数。
int NpyIter_ResetToIterIndexRange( *iter, istart, 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_SUCCEED
或NPY_FAIL
。如果 errmsg
非空,则当返回NPY_FAIL
时不会设置任何 Python 异常。相反,errmsg 会被设置为错误消息。当 errmsg
非空时,可以在不持有 Python GIL 的情况下安全地调用该函数。
int NpyIter_ResetBasePointers( *iter, char **baseptrs, char **errmsg)
将迭代器重置回初始状态,但使用baseptrs
中的值作为数据,而不是被迭代的数组中的指针。这个函数有意与 op_axes
参数一起被嵌套迭代代码使用,其中有两个或更多个迭代器。
返回NPY_SUCCEED
或NPY_FAIL
。如果 errmsg
非空,则当返回NPY_FAIL
时不会设置任何 Python 异常。相反,errmsg 会被设置为错误消息。当 errmsg
非空时,可以在不持有 Python GIL 的情况下安全地调用该函数。
TODO:将以下内容移入关于嵌套迭代器的特殊部分中。
创建嵌套迭代的迭代器需要一些注意。所有的迭代器操作数必须完全匹配,否则调用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( *iter, const *multi_index)
将迭代器调整到由multi_index
指向的ndim
索引。如果未正在追踪多索引,索引超出范围,或内部循环迭代被禁用,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
int NpyIter_GotoIndex( *iter, index)
将迭代器调整到指定的index
。如果迭代器是使用标记NPY_ITER_C_INDEX
构造的,则index
是 C 顺序索引;如果迭代器是使用标记NPY_ITER_F_INDEX
构造的,则index
是 Fortran 顺序索引。如果没有正在追踪的索引,索引超出范围,或者内部循环迭代被禁用,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_GetIterSize( *iter)
返回正在迭代的元素数量。这是形状中所有维度的乘积。当正在追踪多个索引(并且可能调用NpyIter_RemoveAxis)时,大小可能为-1
,表示迭代器过大。这样的迭代器无效,但在调用NpyIter_RemoveAxis后可能变为有效。不必检查这种情况。
NpyIter_GetIterIndex( *iter)
获取迭代器的iterindex
,即与迭代器的迭代顺序匹配的索引。
void NpyIter_GetIterIndexRange( *iter, *istart, *iend)
获取正在迭代的iterindex
子范围。如果未指定标记NPY_ITER_RANGED
,则始终返回范围0, NpyIter_IterSize(iter))
。
int NpyIter_GotoIterIndex( *iter, iterindex)
调整迭代器以指向指定的iterindex
。IterIndex 是将迭代器的迭代顺序匹配的索引。如果iterindex
超出范围,启用了缓冲,或内部循环迭代被禁用,则返回错误。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_HasDelayedBufAlloc( *iter)
如果标记[NPY_ITER_DELAY_BUFALLOC
被传递给迭代器构造函数,并且还没有调用其中一个 Reset 函数,则返回 1;否则返回 0。
NpyIter_HasExternalLoop( *iter)
根据构造函数标记NPY_ITER_EXTERNAL_LOOP
或NpyIter_EnableExternalLoop
,返回 1 表示调用者需要处理最内层的一维循环,返回 0 表示迭代器处理所有循环。
NpyIter_HasMultiIndex( *iter)
如果迭代器是使用标记NPY_ITER_MULTI_INDEX
创建的,则返回 1,否则返回 0。
NpyIter_HasIndex( *iter)
如果迭代器是使用标记NPY_ITER_C_INDEX
或NPY_ITER_F_INDEX
创建的,则返回 1,否则返回 0。
NpyIter_RequiresBuffering( *iter)
如果迭代器需要缓冲,则返回 1,即当操作数需要转换或对齐时无法直接使用。
NpyIter_IsBuffered( *iter)
如果迭代器是使用标记NPY_ITER_BUFFERED
创建的,则返回 1,否则返回 0。
NpyIter_IsGrowInner( *iter)
如果迭代器是使用标记NPY_ITER_GROWINNER
创建的,则返回 1,否则返回 0。
NpyIter_GetBufferSize( *iter)
如果迭代器已缓冲,则返回正在使用的缓冲区的大小,否则返回 0。
int NpyIter_GetNDim( *iter)
返回正在迭代的维数。如果在迭代器构造函数中未请求多索引,则此值可能小于原始对象中的维数。
int NpyIter_GetNOp( *iter)
返回迭代器中的操作数数量。
*NpyIter_GetAxisStrideArray( *iter, int axis)
获取指定轴的步幅数组。要求迭代器跟踪多索引,并且缓冲未启用。
在您希望以某种方式匹配操作数轴,然后使用 NpyIter_RemoveAxis
手动处理它们时,可以使用此函数。在移除轴之前调用此函数,可以获取手动处理的步幅。
错误时返回NULL
。
int NpyIter_GetShape( *iter, *outshape)
在outshape
中返回迭代器的广播形状。只能在跟踪多索引的迭代器上调用此函数。
返回NPY_SUCCEED
或NPY_FAIL
。
**NpyIter_GetDescrArray( *iter)
这会返回正在迭代的对象的nop
数据类型 Descrs 的指针。结果指向iter
,因此调用者不会获得任何对 Descrs 的引用。
在迭代循环之前,此指针可以被缓存,调用iternext
不会更改它。
**NpyIter_GetOperandArray( *iter)
这会将指针返回到正在迭代的nop
操作数 PyObjects。结果指向iter
,因此调用者不会获得任何对 PyObjects 的引用。
*NpyIter_GetIterView( *iter, i)
将返回一个新的 ndarray 视图的引用,该视图是数组NpyIter_GetOperandArray
中第 i 个对象的视图,其维度和步幅与内部优化的迭代模式相匹配。此视图的 C 顺序迭代等同于迭代器的迭代顺序。
例如,如果使用单个数组作为输入创建了迭代器,并且可能对其所有轴进行重排,然后将其合并为一个单一的跨度迭代,这将返回一个视图,即一维数组。
void NpyIter_GetReadFlags( *iter, char *outreadflags)
填充nop
标志。如果可以从op[i]
读取,则将outreadflags[i]
设置为 1,否则设置为 0。
void NpyIter_GetWriteFlags( *iter, char *outwriteflags)
填充nop
标志。如果可以向op[i]
写入,则将outwriteflags[i]
设置为 1,否则设置为 0。
int NpyIter_CreateCompatibleStrides( *iter, itemsize, *outstrides)
构建与使用 NPY_ITER_ALLOCATE
标志创建的输出数组的步幅相同的步幅集,其中为 op_axes 传递了 NULL。这用于连续打包的数据,但不一定按照 C 或 Fortran 顺序。这应该与传递到构造函数的标志 NPY_ITER_MULTI_INDEX
一起使用,以及 NpyIter_GetShape
和 NpyIter_GetNDim
。
此函数的用例是匹配迭代器的形状和布局,并附加一个或多个维度。例如,为了为数值梯度每个输入值生成一个向量,您需要传入 ndim*itemsize 作为 itemsize,然后在末尾添加另一个大小为 ndim,步幅为 itemsize 的维度。对于 Hessian 矩阵,做同样的事情,但是增加两个维度,或者利用对称性并将其打包到一个特定编码的维度中。
只有在迭代器正在跟踪多索引并且使用了NPY_ITER_DONT_NEGATE_STRIDES
以防止轴逆序迭代时,才能调用此函数。
如果使用此方法创建数组,则仅需添加每次迭代的‘itemsize’即可遍历与迭代器匹配的新数组。
返回NPY_SUCCEED
或NPY_FAIL
。
NpyIter_IsFirstVisit( *iter, int iop)
版本 1.7 中的新内容。
检查是否是迭代器指向的指定减少操作数的元素第一次被看到。该函数对于减少操作数和禁用缓冲的情况会返回一个合理的答案。对于缓冲的非减少操作数,答案可能是不正确的。
此函数仅用于 EXTERNAL_LOOP 模式,并且在未启用该模式时将产生一些错误的答案。
如果此函数返回 true,则调用者还应检查操作数的内循环步幅,因为如果该步幅为 0,则仅首次访问最内层外部循环的第一个元素。
警告:出于性能原因,’iop’没有进行边界检查,没有确认‘iop’实际上是减少操作数,也没有确认是否启用了 EXTERNAL_LOOP 模式。这些检查是调用者的责任,并应在任何内部循环之外完成。
迭代函数
*NpyIter_GetIterNext( *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_BUFFERED
和NPY_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_GetGetMultiIndex( *iter, char **errmsg)
返回获取迭代器当前多索引的函数指针。如果迭代器未跟踪多索引,则返回 NULL。建议在迭代循环之前将此函数指针缓存到一个本地变量中。
如果发生错误,则返回 NULL。如果NPY_FAIL
返回,则不设置 Python 异常。相反,errmsg 被设置为错误消息。当errmsg非 NULL 时,可以安全地调用该函数,而无需持有 Python GIL。
char **NpyIter_GetDataPtrArray( *iter)
这将返回一个指向nop
数据指针的指针。如果未指定NPY_ITER_EXTERNAL_LOOP
,则每个数据指针指向迭代器的当前数据项。如果未指定内部迭代,则指向内部循环的第一个数据项。
此指针可能在迭代循环之前被缓存,调用iternext
不会改变它。此函数可在不持有 Python GIL 的情况下安全调用。
char **NpyIter_GetInitialDataPtrArray( *iter)
直接获取数据指针数组进入数组(永远不会进入缓冲区),对应于迭代索引 0。
这些指针与NpyIter_ResetBasePointers
接受的指针不同,因为某些轴上的方向可能已被反转。
此函数可在不持有 Python GIL 的情况下安全调用。
*NpyIter_GetIndexPtr( *iter)
这将返回一个指针,指向正在跟踪的索引,如果没有跟踪索引,则返回 NULL。仅当在构造期间指定了标志NPY_ITER_C_INDEX
或NPY_ITER_F_INDEX
之一时才可用。
当使用标志NPY_ITER_EXTERNAL_LOOP
时,代码需要知道执行内部循环的参数。这些函数提供了该信息。
*NpyIter_GetInnerStrideArray( *iter)
返回指向nop
步长数组的指针,每个迭代对象一个,用于内部循环使用。
此指针可能在迭代循环之前被缓存,调用iternext
不会改变它。此函数可在不持有 Python GIL 的情况下安全调用。
警告:虽然指针可能被缓存,但如果迭代器被缓冲,其值可能会发生变化。
*NpyIter_GetInnerLoopSizePtr( *iter)
返回指针,指向内部循环应执行的迭代次数。
此地址可以在迭代循环之前进行缓存,调用iternext
不会更改它。值本身可能会在迭代过程中更改,特别是如果启用了缓冲。可以在没有持有 Python GIL 的情况下安全调用此函数。
void NpyIter_GetInnerFixedStrideArray( *iter, *out_strides)
获取一个在整个迭代过程中是固定或不会更改的步长数组。对于可能会更改的步长,会将值 NPY_MAX_INTP 放入步长中。
一旦迭代器准备好进行迭代(如果使用了NPY_ITER_DELAY_BUFALLOC
后的重置),调用此函数以获取可用于选择快速内循环函数的步长。例如,如果步长为 0,这意味着内循环总是可以将其值加载到变量中一次,然后在整个循环中使用该变量,或者如果步长等于项目大小,则该操作数的连续版本可能会被使用。
可以在没有持有 Python GIL 的情况下安全调用此函数。
从先前的 NumPy 迭代器进行转换
旧的迭代器 API 包括函数如 PyArrayIter_Check,PyArray_Iter和 PyArray_ITER_。多迭代器数组包括 PyArray_MultiIter*,PyArray_Broadcast 和 PyArray_RemoveSmallest。新的迭代器设计将所有这些功能及其相关 API 替换为单个对象。新 API 的一个目标是,现有迭代器的所有用法都应该可以在不费力的情况下用新迭代器替代。在 1.6 中,对此的主要例外是邻域迭代器,在这个迭代器中没有相应的功能。
以下是用于新迭代器的函数使用转换表:
迭代器函数 | |
---|---|
PyArray_IterNew |
NpyIter_New |
PyArray_IterAllButAxis |
NpyIter_New + axes 参数或迭代器标志NPY_ITER_EXTERNAL_LOOP |
PyArray_BroadcastToShape |
不支持(请改用多个操作数的支持) |
PyArrayIter_Check |
需要在 Python 公开中添加此内容 |
PyArray_ITER_RESET |
NpyIter_Reset |
PyArray_ITER_NEXT |
从NpyIter_GetIterNext 得到的函数指针 |
PyArray_ITER_DATA |
NpyIter_GetDataPtrArray |
PyArray_ITER_GOTO |
NpyIter_GotoMultiIndex |
PyArray_ITER_GOTO1D |
NpyIter_GotoIndex 或 NpyIter_GotoIterIndex |
PyArray_ITER_NOTDONE |
iternext 函数指针的返回值 |
多迭代器函数 | |
PyArray_MultiIterNew |
NpyIter_MultiNew |
PyArray_MultiIter_RESET |
NpyIter_Reset |
PyArray_MultiIter_NEXT |
来自 NpyIter_GetIterNext 的函数指针 |
PyArray_MultiIter_DATA |
NpyIter_GetDataPtrArray |
PyArray_MultiIter_NEXTi |
不支持(总是锁步迭代) |
PyArray_MultiIter_GOTO |
NpyIter_GotoMultiIndex |
PyArray_MultiIter_GOTO1D |
NpyIter_GotoIndex 或 NpyIter_GotoIterIndex |
PyArray_MultiIter_NOTDONE |
iternext 函数指针的返回值 |
PyArray_Broadcast |
由 NpyIter_MultiNew 处理 |
PyArray_RemoveSmallest |
迭代器标记 NPY_ITER_EXTERNAL_LOOP |
其他功能 | |
PyArray_ConvertToCommonType |
迭代器标记 NPY_ITER_COMMON_DTYPE |
UFunc API
常量
UFUNC_ERR_{HANDLER}
UFUNC_ERR_IGNORE
UFUNC_ERR_WARN
UFUNC_ERR_RAISE
UFUNC_ERR_CALL
UFUNC_{THING}_{ERR}
UFUNC_MASK_DIVIDEBYZERO
UFUNC_MASK_OVERFLOW
UFUNC_MASK_UNDERFLOW
UFUNC_MASK_INVALID
UFUNC_SHIFT_DIVIDEBYZERO
UFUNC_SHIFT_OVERFLOW
UFUNC_SHIFT_UNDERFLOW
UFUNC_SHIFT_INVALID
UFUNC_FPE_DIVIDEBYZERO
UFUNC_FPE_OVERFLOW
UFUNC_FPE_UNDERFLOW
UFUNC_FPE_INVALID
PyUFunc_{VALUE}
PyUFunc_One
PyUFunc_Zero
PyUFunc_MinusOne
PyUFunc_ReorderableNone
PyUFunc_None
PyUFunc_IdentityValue
宏
NPY_LOOP_BEGIN_THREADS
用于通用函数代码,仅当 loop->obj 不为真时释放 Python GIL(即这不是一个对象数组循环)。在变量声明区域需要使用NPY_BEGIN_THREADS_DEF
。
NPY_LOOP_END_THREADS
用于通用函数代码,如果释放了 Python GIL(因为 loop->obj 不为真),则重新获取它。
类型
type PyUFuncGenericFunction
实际实现底层(逐元素)函数的指针,带有以下签名,并进行(N)次操作:
void loopfunc(char **args, const *dimensions, const *steps, void *data)
参数
指向输入和输出数组实际数据的指针数组。先给出输入参数,然后是输出参数。
维度
指向此函数循环的维度大小的指针。
步骤
指向每个输入和输出参数中获取下一个元素所需的字节数的指针。
数据
可以与 ufunc 一起存储的任意数据(额外参数、函数名称等)。在调用时将传递给 ufunc。可以是
NULL
。版本 1.23.0 中更改:接受
NULL
data以及NULL
值数组。
这是专门为返回双精度浮点数的双精度浮点数加法的函数的示例。
static void
double_add(char **args,
npy_intp const *dimensions,
npy_intp const *steps,
void *extra)
{
npy_intp i;
npy_intp is1 = steps[0], is2 = steps[1];
npy_intp os = steps[2], n = dimensions[0];
char *i1 = args[0], *i2 = args[1], *op = args[2];
for (i = 0; i < n; i++) {
*((double *)op) = *((double *)i1) +
*((double *)i2);
i1 += is1;
i2 += is2;
op += os;
}
}
函数
*PyUFunc_FromFuncAndData( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused)
从所需变量创建新的广播通用函数。每个 ufunc 都围绕逐元素操作的概念构建。每个 ufunc 对象都包含指向 1-d 循环的指针,实现了每种支持类型的基本功能。
注意
通过PyUFunc_FromFuncAndData
不会复制func、data、types、name和doc参数。调用者必须确保只要 ufunc 对象存活,这些数组使用的内存不会被释放。
参数:
-
func – 必须指向包含ntypes
PyUFuncGenericFunction
元素的数组。 -
数据 – 应为
NULL
或指向ntypes大小的数组的指针。这个数组可能包含要传递给 func 数组中相应循环函数的任意额外数据,包括NULL
。 -
类型 –
长度为
(nin + nout) * ntypes
的char
编码数组,编码了func
数组中的相应函数所接受的numpy.dtype.num
(仅限内置)。例如,对于具有三个ntypes
、两个nin
和一个nout
的比较 ufunc,其中第一个函数接受numpy.int32
,第二个函数接受numpy.int64
,两者都返回numpy.bool_
,types
将是(char[]) {5, 5, 0, 7, 7, 0}
,因为NPY_INT32
为 5,NPY_INT64
为 7,而NPY_BOOL
为 0。如有需要,也可以使用位宽名称(例如
NPY_INT32
、NPY_COMPLEX128
)。在运行时将使用类型转换规则来查找由输入/输出提供的第一个可调用函数。
-
ntypes – 该 ufunc 已实现的不同数据类型特定函数的数量。
-
nin – 此操作的输入数目。
-
nout – 输出数目。
-
identity – 可以是
PyUFunc_One
、PyUFunc_Zero
、PyUFunc_MinusOne
或PyUFunc_None
。当将空数组传递给 ufunc 的 reduce 方法时,指定应该返回什么。特殊值PyUFunc_IdentityValue
只能与PyUFunc_FromFuncAndDataAndSignatureAndIdentity
方法一起使用,以允许任意 Python 对象用作 identity。 -
name – 作为以
NULL
结尾的字符串的 ufunc 的名称。指定为“add”或“multiply”的名称将在没有给出 dtype 时启用特殊行为,当输入类型为小于numpy.int_
数据类型大小的整型(或布尔型)数据类型时,将内部将其提升为numpy.int_
(或numpy.uint
)数据类型。 -
doc – 允许传递一个文档字符串以与 ufunc 一起存储。文档字符串不应包含函数的名称或调用签名,因为这将从对象中动态确定,并在访问 ufunc 的doc属性时可用。
-
unused – 未使用,仅用于保留 C-API 的向后兼容性。
*PyUFunc_FromFuncAndDataAndSignature( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused, char *signature)
此函数与上面的 PyUFunc_FromFuncAndData 非常相似,但多了一个signature参数,用于定义广义通用函数。与通用函数围绕逐元素操作构建的方式类似,广义通用函数围绕子数组逐个子数组操作展开,signature 定义了要操作的子数组。
参数:
- signature – 新 gufunc 的签名。将其设置为 NULL 等效于调用 PyUFunc_FromFuncAndData。对字符串进行副本,因此传入的缓冲区可被释放。
*PyUFunc_FromFuncAndDataAndSignatureAndIdentity( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused, char *signature, *identity_value)
此函数与上述PyUFunc_FromFuncAndDataAndSignature非常相似,但多了一个identity_value参数,用于在identity
作为PyUFunc_IdentityValue
时为 ufunc 定义任意标识。
参数:
- identity_value – 新 gufunc 的标识。除非
identity
参数为PyUFunc_IdentityValue
,否则必须传递为NULL
。将其设置为 NULL 等效于调用 PyUFunc_FromFuncAndDataAndSignature。
int PyUFunc_RegisterLoopForType( *ufunc, int usertype, function, int *arg_types, void *data)
此函数允许用户将已创建的 ufunc 注册的 1-d 循环替换为在调用该 ufunc 时使用任何输入参数为用户定义数据类型时使用的循环。这是为了使通用函数与内置数据类型一起使用。数据类型必须先前在 numpy 系统中注册。将循环传递为function。这个循环可以接受应作为data传递的任意数据。循环需要的数据类型作为arg_types传递,arg_types必须是至少与 ufunc->nargs 一样大的内存地址的指针。
int PyUFunc_RegisterLoopForDescr( *ufunc, *userdtype, function, **arg_dtypes, void *data)
此函数与上面的 PyUFunc_RegisterLoopForType 非常相似,但允许用户使用 PyArray_Descr 对象注册 1-d 循环,而不是 dtype 类型数字值。这允许为结构化数组数据类型和自定义数据类型注册 1-d 循环,而不是标量数据类型。
int PyUFunc_ReplaceLoopBySignature( *ufunc, newfunc, int *signature, *oldfunc)
用新的 1-d 循环 newfunc 替换已创建的ufunc中匹配给定signature的 1-d 循环。在oldfunc中返回旧的 1-d 循环函数。成功返回 0,失败返回-1。此函数仅适用于内置类型(对于用户定义类型,请使用PyUFunc_RegisterLoopForType
)。签名是一个数据类型数字数组,指示 1-d 循环所假设的输入,然后是输出。
int PyUFunc_checkfperr(int errmask, *errobj)
提供一个简单的接口以支持 IEEE 错误标志的检查。 errmask 参数是一个由 UFUNC_MASK_{ERR}
位掩码组成的掩码,指示要检查哪些错误(以及如何检查这些错误)。 errobj 必须是一个包含两个元素的 Python 元组:一个包含在任何错误通信中使用的名称的字符串,以及一个可调用的 Python 对象(回调函数)或 Py_None
。 只有在将 UFUNC_ERR_CALL
设置为所需的错误检查方法时才会使用可调用对象。 此例程管理 GIL,并且即使在释放 GIL 后仍然安全调用。 如果确定 IEEE 兼容硬件中存在错误,则返回 -1,否则返回 0。
void PyUFunc_clearfperr()
清除 IEEE 错误标志。
void PyUFunc_GetPyValues(char *name, int *bufsize, int *errmask, **errobj)
从线程本地存储区获取用于 ufunc 处理的 Python 值,除非设置了默认值,在这种情况下名称查找将被绕过。 名称作为字符串放置在 *errobj 的第一个元素中。 第二个元素是在错误回调时调用的查找函数。 要使用的查找缓冲区大小的值传递给 bufsize,并将错误掩码的值放入 errmask。
通用函数
每个 ufunc 的核心是一组类型特定函数,定义了每个支持类型的基本功能。 这些函数必须评估底层函数 (N\geq1) 次。 可以传递额外数据,该数据在计算期间可能被使用。 此功能允许将一些常规函数用作这些基本循环函数。 一般函数具有指向正确位置的变量的所有所需代码,并设置函数调用。 一般函数假定传入的实际要调用的函数作为额外数据,并调用它以正确的值。 所有这些函数都适用于直接放置在 PyUFuncObject 结构的 functions 成员中存储的函数数组中。
void PyUFunc_f_f_As_d_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_d_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_f_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_g_g(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_F_F_As_D_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_F_F(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_D_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_G_G(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e_As_f_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e_As_d_d(char **args, const *dimensions, const *steps, void *func)
为 ufunc 提供特定于类型的核心 1-d 函数,其中每个计算通过调用一个接受一个输入参数并返回一个输出的函数来获得。 此函数传递给 func
。 字母对应于支持的数据类型的 dtypechar( e
- 半精度浮点数, f
- 浮点数, d
- 双精度浮点数, g
- 长双精度浮点数, F
- 复数浮点数, D
- 复数双精度浮点数, G
- 复数长双精度浮点数)。 参数 func 必须支持相同的签名。 _As_X_X 变体假定单一数据类型的 ndarray 但将值转换为使用接受不同数据类型的基础函数的功能。 因此,PyUFunc_f_f_As_d_d
使用数据类型为 NPY_FLOAT
的 ndarray,但调用一个接受 double 并返回 double 的 C 函数。
void PyUFunc_ff_f_As_dd_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ff_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_dd_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_gg_g(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_FF_F_As_DD_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_DD_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_FF_F(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_GG_G(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e_As_ff_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e_As_dd_d(char **args, const *dimensions, const *steps, void *func)
针对 ufunc 的特定类型的核心 1 维函数,其中每个计算都通过调用一个接受两个输入参数并返回一个输出参数的函数获得。要调用的底层函数传递为func。字母对应于通用函数支持的特定数据类型的 dtypechar。参数func
必须支持相应的签名。_As_XX_X
变体假设一维数组是一种数据类型,但在每次循环迭代时将值转换为使用接受不同数据类型的底层函数。
void PyUFunc_O_O(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_OO_O(char **args, const *dimensions, const *steps, void *func)
用于NPY_OBJECT
数据类型的一输入、一输出和二输入、一输出核心 1 维函数。这些函数处理引用计数问题,并在错误发生时提前返回。要调用的实际函数是func,它必须接受带有签名(PyObject*) (PyObject*)
的调用,用于PyUFunc_O_O
或带有签名(PyObject*)(PyObject *, PyObject *)
的调用,用于PyUFunc_OO_O
。
void PyUFunc_O_O_method(char **args, const *dimensions, const *steps, void *func)
这个通用的 1 维核心函数假定func是表示输入对象方法的字符串。对于循环的每次迭代,从数组中提取 Python 对象,并调用其func方法,将结果返回到输出数组。
void PyUFunc_OO_O_method(char **args, const *dimensions, const *steps, void *func)
这个通用的 1 维核心函数假定func是表示输入对象方法的字符串,该方法接受一个参数。args中的第一个参数是被调用的函数,args中的第二个参数是传递给函数的参数。函数的输出存储在args的第三个条目中。
void PyUFunc_On_Om(char **args, const *dimensions, const *steps, void *func)
这是由 umath.frompyfunc(function, nin, nout)创建的动态 ufunc 使用的 1 维核心函数。在这种情况下,func是一个指向PyUFunc_PyFuncData
结构的指针,其定义为
type PyUFunc_PyFuncData
typedef struct {
int nin;
int nout;
PyObject *callable;
} PyUFunc_PyFuncData;
在每次循环迭代中,从其对象数组中提取nin个输入对象,并将其放入参数元组中,然后使用 Python callable调用输入参数,并将 nout 输出放入其对象数组中。
导入 API
PY_UFUNC_UNIQUE_SYMBOL
NO_IMPORT_UFUNC
void import_ufunc(void)
这些是用于从扩展模块中精确地访问 ufunc C-API 的常量和函数,就像可以访问数组 C-API 一样。import_ufunc
()函数必须始终被调用(在扩展模块的初始化子例程中)。如果您的扩展模块在一个文件中,则这就是所需的全部。如果您的扩展模块使用多个文件,则另外两个常量很有用。在这种情况下,将PY_UFUNC_UNIQUE_SYMBOL
定义为与您的代码唯一对应的内容,然后在不包含模块初始化函数但仍需要访问 UFUNC API 的源文件中,将PY_UFUNC_UNIQUE_SYMBOL
定义为先前使用的相同名称,并同时定义NO_IMPORT_UFUNC
。
C-API 实际上是一个函数指针数组。此数组由 import_ufunc 创建(并由全局变量指向)。全局变量是静态定义的或允许被其他文件看到,具体取决于PY_UFUNC_UNIQUE_SYMBOL
和NO_IMPORT_UFUNC
的状态。
常量
UFUNC_ERR_{HANDLER}
UFUNC_ERR_IGNORE
UFUNC_ERR_WARN
UFUNC_ERR_RAISE
UFUNC_ERR_CALL
UFUNC_{THING}_{ERR}
UFUNC_MASK_DIVIDEBYZERO
UFUNC_MASK_OVERFLOW
UFUNC_MASK_UNDERFLOW
UFUNC_MASK_INVALID
UFUNC_SHIFT_DIVIDEBYZERO
UFUNC_SHIFT_OVERFLOW
UFUNC_SHIFT_UNDERFLOW
UFUNC_SHIFT_INVALID
UFUNC_FPE_DIVIDEBYZERO
UFUNC_FPE_OVERFLOW
UFUNC_FPE_UNDERFLOW
UFUNC_FPE_INVALID
PyUFunc_{VALUE}
PyUFunc_One
PyUFunc_Zero
PyUFunc_MinusOne
PyUFunc_ReorderableNone
PyUFunc_None
PyUFunc_IdentityValue
宏
NPY_LOOP_BEGIN_THREADS
仅在通用函数代码中使用,如果 loop->obj 不为真(即这不是一个 OBJECT 数组循环),则只释放 Python GIL。在变量声明区域中需要使用NPY_BEGIN_THREADS_DEF
。
NPY_LOOP_END_THREADS
用于在循环->obj 不为真时(因为循环->obj 不为真)重新获取 Python GIL 的指针。
类型
type PyUFuncGenericFunction
实际实现底层(逐元素)函数的函数指针数组。具有以下签名的 N 次循环:
void loopfunc(char **args, const *dimensions, const *steps, void *data)
参数
指向输入和输出数组的实际数据的指针数组。首先是输入参数,然后是输出参数。
维度
指向此函数循环的维度大小的指针。
步骤
指向每个输入和输出参数在该维度中跳到下一个元素的字节数的指针。
数据
可以与 ufunc 一起存储并在调用时传递的任意数据(额外参数、函数名称、等)。可以是
NULL
。在 1.23.0 版本中更改:接受
NULL
data以及NULL
值数组。
这是一个专门用于返回双精度的双精度加法的函数示例。
static void
double_add(char **args,
npy_intp const *dimensions,
npy_intp const *steps,
void *extra)
{
npy_intp i;
npy_intp is1 = steps[0], is2 = steps[1];
npy_intp os = steps[2], n = dimensions[0];
char *i1 = args[0], *i2 = args[1], *op = args[2];
for (i = 0; i < n; i++) {
*((double *)op) = *((double *)i1) +
*((double *)i2);
i1 += is1;
i2 += is2;
op += os;
}
}
函数
*PyUFunc_FromFuncAndData( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused)
从所需变量创建新的广播通用函数。每个 ufunc 都围绕着逐元素操作的概念构建。每个 ufunc 对象都包含指向为每种支持类型实现基本功能的 1-d 循环的指针。
注意
func、data、types、name和doc参数不会被PyUFunc_FromFuncAndData
复制。调用者必须确保只要 ufunc 对象存在,这些数组使用的内存不会被释放。
参数:
-
func – 必须指向一个包含ntypes
PyUFuncGenericFunction
元素的数组。 -
data – 应为
NULL
或大小为ntypes的数组指针。该数组可以包含任意额外数据,以传递给 func 数组中对应的循环函数,包括NULL
。 -
types –
长度为
(nin + nout) * ntypes
的char
数组,编码对应于func
数组中的函数所接受的numpy.dtype.num
(仅内置)。例如,对于一个具有三种ntypes
,两个nin
和一个nout
的比较 ufunc,其中第一个函数接受numpy.int32
,第二个接受numpy.int64
,两者都返回numpy.bool_
,types
将是(char[]) {5, 5, 0, 7, 7, 0}
,因为NPY_INT32
是 5,NPY_INT64
是 7,NPY_BOOL
是 0。位宽度名称也可以使用(例如
NPY_INT32
,NPY_COMPLEX128
)。类型转换规则将在运行时用于查找由提供的输入/输出找到的第一个
func
可调用函数。 -
ntypes – ufunc 已实现的不同数据类型特定函数的数量。
-
nin – 此操作的输入数目。
-
nout – 输出的数目
-
identity – 要么是
PyUFunc_One
,PyUFunc_Zero
,PyUFunc_MinusOne
,或PyUFunc_None
。这指定了当将空数组传递给 ufunc 的 reduce 方法时应返回的内容。特殊值PyUFunc_IdentityValue
只能与PyUFunc_FromFuncAndDataAndSignatureAndIdentity
方法一起使用,以允许使用任意 Python 对象作为标识。 -
name – 作为
NULL
终止的字符串的 ufunc 名称。指定名称为‘add’或‘multiply’时,当没有给出 dtype 时,会启用整数类型缩减的特殊行为。如果输入类型是小于numpy.int_
数据类型大小的整数(或布尔)数据类型,它将在内部转换为numpy.int_
(或numpy.uint
)数据类型。 -
doc – 允许传入文档字符串以与 ufunc 存储。文档字符串不应包含函数名称或调用签名,因为这将从对象动态确定,并在访问 ufunc 的 doc 属性时可用。
-
unused – 未使用,为了保持 C-API 的向后兼容性而存在。
*PyUFunc_FromFuncAndDataAndSignature( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused, char *signature)
此函数与上述的 PyUFunc_FromFuncAndData 非常相似,但具有额外的 signature 参数,用于定义广义通用函数。与 ufuncs 围绕逐元素操作构建类似,gufuncs 围绕子数组逐个子数组操作构建,signature 定义了要操作的子数组。
参数:
- signature – 新 gufunc 的签名。将其设置为 NULL 相当于调用 PyUFunc_FromFuncAndData。会复制字符串,因此传入的缓冲区可以释放。
*PyUFunc_FromFuncAndDataAndSignatureAndIdentity( *func, void **data, char *types, int ntypes, int nin, int nout, int identity, char *name, char *doc, int unused, char *signature, *identity_value)
此函数与上述的 PyUFunc_FromFuncAndDataAndSignature 非常相似,但具有额外的 identity_value 参数,用于在 identity
为 PyUFunc_IdentityValue
时定义 ufunc 的任意标识。
参数:
- identity_value – 新 gufunc 的标识。除非
identity
参数为PyUFunc_IdentityValue
,否则必须传递为NULL
。将其设置为 NULL 相当于调用 PyUFunc_FromFuncAndDataAndSignature。
int PyUFunc_RegisterLoopForType( *ufunc, int usertype, function, int *arg_types, void *data)
此函数允许用户使用已创建的 ufunc 注册 1-d 循环,以便在任何输入参数作为用户定义的数据类型调用 ufunc 时使用。为了使 ufuncs 与内置数据类型一起工作,需要这样做。数据类型必须已在 numpy 系统中注册。循环传递为 function。此循环可以获取任意数据,应将其作为 data 传递。循环所需的数据类型作为 arg_types 传入,它必须是至少与 ufunc->nargs 一样大的内存指针。
int PyUFunc_RegisterLoopForDescr( *ufunc, *userdtype, function, **arg_dtypes, void *data)
此函数的行为类似于上面的 PyUFunc_RegisterLoopForType,但允许用户使用 PyArray_Descr 对象而不是 dtype 类型 num 值注册 1-d 循环。这允许为结构化数组数据类型和自定义数据类型注册 1-d 循环,而不是标量数据类型。
int PyUFunc_ReplaceLoopBySignature( *ufunc, newfunc, int *signature, *oldfunc)
在已创建的 ufunc 中替换与给定 signature 匹配的 1-d 循环为新的 1-d 循环 newfunc。在 oldfunc 中返回旧的 1-d 循环函数。成功返回 0,失败返回 -1。此函数仅适用于内置类型(对于用户定义类型,请使用 PyUFunc_RegisterLoopForType
)。签名是指示输入后跟 1-d 循环所假设的输出的数据类型编号数组。
int PyUFunc_checkfperr(int errmask, *errobj)
提供了一个简单的接口来检查 IEEE 错误标志的支持。errmask 参数是 UFUNC_MASK_{ERR}
位掩码的组合,指示要检查哪些错误(以及如何检查)。errobj 必须是一个 Python 元组,有两个元素:一个字符串,包含在任何错误通信中将使用的名称,以及一个可调用的 Python 对象(回调函数)或 Py_None
。只有在设置 UFUNC_ERR_CALL
作为所需错误检查方法时,才会使用可调用对象。此例程管理 GIL 并且即使在释放 GIL 后也是安全的。如果确定 IEEE 兼容硬件中存在错误,则返回 -1,否则返回 0。
void PyUFunc_clearfperr()
清除 IEEE 错误标志。
void PyUFunc_GetPyValues(char *name, int *bufsize, int *errmask, **errobj)
除非默认已设置,否则从线程本地存储区获取用于 ufunc 处理的 Python 值,此时会绕过名称查找。将名称作为字符串放置在 *errobj 的第一个元素中。第二个元素是在错误回调时要调用的查找函数。传递要使用的查找缓冲区大小的值,并将错误掩码的值放置到 errmask 中。
通用函数
每个 ufunc 的核心是一组特定于类型的函数,定义了每个支持类型的基本功能。这些函数必须对底层函数进行 (N\geq1) 次评估。可能传递附加数据,在计算过程中可能使用。此功能允许将一些通用函数用作这些基本循环函数。通用函数具有指向正确位置的变量和设置函数调用的所有所需代码。通用函数假定实际要调用的函数作为附加数据传递,并使用正确的值调用它。所有这些函数都适合直接放置在 PyUFuncObject 结构的 functions 成员中存储的函数数组中。
void PyUFunc_f_f_As_d_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_d_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_f_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_g_g(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_F_F_As_D_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_F_F(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_D_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_G_G(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e_As_f_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_e_e_As_d_d(char **args, const *dimensions, const *steps, void *func)
Type 特定的 1-d 核心函数用于 ufuncs,每个计算都是通过调用一个输入参数并返回一个输出的函数来获得的。这个函数传递给func
。这些字母对应于支持的数据类型的 dtypechar ( e
- half, f
- float, d
- double, g
- long double, F
- cfloat, D
- cdouble, G
- clongdouble)。参数* func*必须支持相同的签名。_As_X_X 变体假定一个数据类型的 ndarray,但将值强制转换为使用一个不同类型的数据类型的基础函数。因此,PyUFunc_f_f_As_d_d
使用数据类型的 ndarray NPY_FLOAT
但调用一个接受 double 并返回 double 的 C 函数。
void PyUFunc_ff_f_As_dd_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ff_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_dd_d(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_gg_g(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_FF_F_As_DD_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_DD_D(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_FF_F(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_GG_G(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e_As_ff_f(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_ee_e_As_dd_d(char **args, const *dimensions, const *steps, void *func)
Type 特定的核心 1-d 函数用于 ufuncs,其中每个计算都是通过调用传入两个输入参数并返回一个输出的函数来获得的。传递要调用的基础函数,字符串作为* func。这些字母对应于特定数据类型的 dtypechar 的通用函数支持的。参数 func*必须支持相应的签名。_As_XX_X 变体假定一个数据类型的 ndarray,但将循环的每次迭代的值强制转换为使用使用不同数据类型的基础函数。
void PyUFunc_O_O(char **args, const *dimensions, const *steps, void *func)
void PyUFunc_OO_O(char **args, const *dimensions, const *steps, void *func)
用于 NPY_OBJECT
数据类型的一个输入、一个输出和两个输入、一个输出的核心 1-d 函数。这些函数处理引用计数问题,并在发生错误时尽早返回。要调用的实际函数是* func*,它必须接受具有签名 (PyObject *) (PyObject *)
的 PyUFunc_O_O
或 (PyObject*)(PyObject *, PyObject *)
的 PyUFunc_OO_O
的调用。
void PyUFunc_O_O_method(char **args, const *dimensions, const *steps, void *func)
这个通用 1-d 核心函数假定* func是表示输入对象方法的字符串。对于循环的每次迭代,从数组中提取 Python 对象并调用它的 func*方法将结果返回到输出数组。
void PyUFunc_OO_O_method(char **args, const *dimensions, const *steps, void *func)
这个通用 1-d 核心函数假定* func是表示输入对象的一个参数的方法的字符串。 args中的第一个参数是调用的方法,第二个参数是传递给方法的参数。函数的输出存储在 args*的第三个条目中。
void PyUFunc_On_Om(char **args, const *dimensions, const *steps, void *func)
这是由 umath.frompyfunc(function, nin, nout)创建的动态 ufunc 使用的 1-d 核心功能。在这种情况下,* func* 是指向 PyUFunc_PyFuncData
结构的指针,其定义为
type PyUFunc_PyFuncData
typedef struct {
int nin;
int nout;
PyObject *callable;
} PyUFunc_PyFuncData;
在每次循环迭代中,从它们的对象数组中提取nin输入对象,并放入一个参数元组中,使用输入参数调用 Python callable,并将 nout 输出放入它们的对象数组中。
导入 API
PY_UFUNC_UNIQUE_SYMBOL
NO_IMPORT_UFUNC
void import_ufunc(void)
这些是用于从扩展模块中精确地访问 ufunc C-API 的常量和函数,方式与数组 C-API 可以被访问的方式完全相同。import_ufunc
()函数必须始终被调用(在扩展模块的初始化子程序中)。如果你的扩展模块在一个文件中,那么这就是所需要的全部。如果你的扩展模块使用多个文件,则另外两个常量会有所帮助。在这种情况下,将PY_UFUNC_UNIQUE_SYMBOL
定义为与您的代码唯一的东西,然后在那些不包含模块初始化函数但仍需要访问 UFUNC API 的源文件中,将PY_UFUNC_UNIQUE_SYMBOL
定义为先前使用的相同名称,并且还要定义NO_IMPORT_UFUNC
。
C-API 实际上是一个函数指针数组。这个数组是由 import_ufunc 创建的(并指向一个全局变量)。全局变量是静态定义的,或者根据PY_UFUNC_UNIQUE_SYMBOL
和NO_IMPORT_UFUNC
的状态可以被其他文件看到。
广义通用函数 API
原文:
numpy.org/doc/1.26/reference/c-api/generalized-ufuncs.html
不仅需要在标量函数上循环,还需要在向量(或数组)函数上循环。这个概念在 NumPy 中通过对通用函数(ufuncs)进行泛化来实现。在常规的 ufuncs 中,基本函数被限制为逐元素操作,而广义版本(gufuncs)支持“子数组”按“子数组”操作。Perl 矢量库 PDL 提供类似的功能,在下文中重新使用其术语。
每个广义通用函数都有与之相关的信息,说明输入的“核心”维度是什么,以及相应的输出维度(逐元素 ufunc 的核心维度为零)。所有参数的核心维度列表称为 ufunc 的“签名”。例如,ufunc numpy.add 签名为(),()->()
,定义了两个标量输入和一个标量输出。
另一个示例是函数inner1d(a, b)
,签名为(i),(i)->()
。这将沿着每个输入的最后一个轴应用内积,但保持其余索引不变。例如,当a
的形状为(3, 5, N)
,b
的形状为(5, N)
时,这将返回形状为(3,5)
的输出。基础的基本功能被调用3 * 5
次。在签名中,我们为每个输入指定一个核心维度(i)
,并为输出指定零个核心维度()
,因为它需要两个 1-d 数组并返回一个标量。通过使用相同的名称i
,我们指定两个对应的维度应该是相同大小。
超出核心尺寸的维度被称为“循环”维度。在上面的示例中,这对应于(3, 5)
。
签名确定每个输入/输出数组的维度如何分割为核心和循环维度:
-
在签名中,每个维度与相应传入数组的维度匹配,从形状元组的末尾开始。这些是核心维度,它们必须存在于数组中,否则会引发错误。
-
在签名中分配给相同标签的核心维度(例如
inner1d
中的(i),(i)->()
中的i
)必须具有完全匹配的大小,不执行广播。 -
所有输入中去除核心维度,剩余维度进行广播,定义循环维度。
-
每个输出的形状取决于循环维度加上输出的核心维度
通常情况下,输出中所有核心维度的大小将由输入数组中具有相同标签的核心维度的大小决定。这不是必须的,也可以定义一个签名,其中一个标签在输出中首次出现,尽管在调用这样的函数时必须采取一些预防措施。一个例子是函数euclidean_pdist(a)
,其签名为(n,d)->(p)
,给定一个包含n
个d
维向量的数组,计算它们之间所有唯一的成对欧几里德距离。因此,输出维度p
必须等于n * (n - 1) / 2
,但调用者有责任传入正确大小的输出数组。如果无法从传入的输入或输出数组确定输出的核心维度的大小,则会引发错误。
注意:在 NumPy 1.10.0 之前,存在较少严格的检查:缺少的核心维度会根据需要在形状前添加 1,具有相同标签的核心维度会一起广播,并且未确定的维度将创建为大小为 1 的维度。
定义
基本函数
每个 ufunc 由一个基本函数组成,该函数在数组参数的最小部分上执行最基本的操作(例如,将两个数字相加是将两个数组相加的最基本操作)。ufunc 在数组的不同部分上多次应用基本函数。基本函数的输入/输出可以是向量;例如,inner1d 的基本函数将两个向量作为输入。
签名
签名是描述 ufunc 基本函数的输入/输出维度的字符串。有关更多详细信息,请参阅下面的部分。
核心维度
每个基本函数的输入/输出的维度由其核心维度定义(零核心维度对应于标量输入/输出)。核心维度映射到输入/输出数组的最后维度。
维度名称
维度名称表示签名中的核心维度。不同的维度可以共享一个名称,表示它们具有相同的大小。
维度索引
维度索引是表示维度名称的整数。它根据签名中每个名称的第一次出现的顺序对维度名称进行枚举。
签名详细信息
签名定义了输入和输出变量的“核心”维度,从而也定义了维度的收缩。签名由以下格式的字符串表示:
-
每个输入或输出数组的核心维度由括号中的维度名称列表
(i_1,...,i_N)
表示;标量输入/输出用()
表示。可以使用任何有效的 Python 变量名,而不是i_1
,i_2
等。 -
不同参数的维度列表由
","
分隔。输入/输出参数由"->"
分隔。 -
如果在多个位置使用相同的维度名称,则强制相应维度的相同大小。
签名的正式语法如下:
<Signature> ::= <Input arguments> "->" <Output arguments>
<Input arguments> ::= <Argument list>
<Output arguments> ::= <Argument list>
<Argument list> ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument> ::= "(" <Core dimension list> ")"
<Core dimension list> ::= nil | <Core dimension> |
<Core dimension> "," <Core dimension list>
<Core dimension> ::= <Dimension name> <Dimension modifier>
<Dimension name> ::= valid Python variable name | valid integer
<Dimension modifier> ::= nil | "?"
注意:
-
所有引用都是为了清晰。
-
未修改的共享相同名称的核心维度必须具有相同的大小。每个维度名称通常对应基本函数实现中的一个循环级别。
-
空格将被忽略。
-
数字作为维度名称将冻结该维度为该值。
-
如果名称后缀带有“?”修饰符,则该维度仅在所有输入和输出共享该维度时才是核心维度;否则将被忽略(并由元素函数的大小为 1 的维度替换)。
下面是一些签名的示例:
name | signature | 常见用法 |
---|---|---|
add | (),()->() |
二进制 ufunc |
sum1d | (i)->() |
减少 |
inner1d | (i),(i)->() |
向量-向量乘法 |
matmat | (m,n),(n,p)->(m,p) |
矩阵乘法 |
vecmat | (n),(n,p)->(p) |
向量-矩阵乘法 |
matvec | (m,n),(n)->(m) |
矩阵-向量乘法 |
matmul | (m?,n),(n,p?)->(m?,p?) |
上述四种的组合 |
outer_inner | (i,t),(j,t)->(i,j) |
最后一个维度内积,倒数第二个维度外积,并循环/广播其他维度 |
cross1d | (3),(3)->(3) |
叉积,其中最后一个维度被冻结并且必须为 3 |
最后一个是冻结核心维度的实例,并可用于改善 ufunc 性能
用于实现基本函数的 C-API
当前接口保持不变,PyUFunc_FromFuncAndData
仍可用于实现(专门化的)ufunc,包括标量基本函数。
人们可以使用PyUFunc_FromFuncAndDataAndSignature
声明更通用的 ufunc。参数列表与PyUFunc_FromFuncAndData
相同,还有一个额外的参数指定签名为 C 字符串。
此外,回调函数的类型与以前相同,void (*foo)(char **args, intp *dimensions, intp *steps, void *func)
。调用时,args
是一个长度为nargs
的列表,其中包含所有输入/输出参数的数据。对于标量基本函数,steps
也是长度为nargs
,表示参数的步进。dimensions
是一个指向定义要循环的轴的大小的单个整数的指针。
对于非平凡签名,dimensions
还将包含核心维度的大小,从第二个条目开始。对于每个惟一的维度名称,仅提供一个大小,并且大小根据签名中维度名称的第一次出现给出。
nargs
的前几个元素与标量 ufunc 相同。接下来的元素包含按顺序所有参数的所有核心维度的步进。
例如,考虑一个签名为(i,j),(i)->()
的 ufunc。在这种情况下,args
将包含对输入/输出数组a
、b
、c
的数据的三个指针。此外,dimensions
将为[N, I, J]
,以定义循环的大小N
和核心维度i
和j
的大小I
和J
。最后,steps
将为[a_N, b_N, c_N, a_i, a_j, b_i]
,包含所有必要的步幅。
定义
基本函数
每个 ufunc 由一个执行最基本操作的基本函数组成,该操作在最小的数组部分上执行(例如,将两个数字相加是在两个数组上添加的最基本操作)。ufunc 将基本函数多次应用于数组的不同部分。基本函数的输入/输出可以是向量;例如,inner1d 的基本函数以两个向量作为输入。
签名
签名是描述 ufunc 的基本函数的输入/输出维度的字符串。有关详细信息,请参见下面的部分。
核心维度
基本函数的每个输入/输出的维度由其核心维度定义(零个核心维度对应于标量输入/输出)。核心维度映射到输入/输出数组的最后维度。
维度名称
维度名称表示签名中的核心维度。不同的维度可以共享一个名称,表示它们具有相同的大小。
维度索引
维度索引是表示维度名称的整数。它根据签名中每个名称的第一次出现的顺序列举维度名称。
签名的详细信息
签名定义了输入和输出变量的“核心”维度,并因此定义维度的合并。签名由以下格式的字符串表示:
-
每个输入或输出数组的核心维度由括号内的维度名称列表
(i_1,...,i_N)
表示;标量输入/输出用()
表示。可以使用任何有效的 Python 变量名称代替i_1
、i_2
等。 -
不同参数的维度列表由逗号
","
分隔。输入/输出参数由箭头"->"
分隔。 -
如果在多个位置使用相同的维度名称,这将强制相应维度的相同大小。
签名的形式语法如下:
<Signature> ::= <Input arguments> "->" <Output arguments>
<Input arguments> ::= <Argument list>
<Output arguments> ::= <Argument list>
<Argument list> ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument> ::= "(" <Core dimension list> ")"
<Core dimension list> ::= nil | <Core dimension> |
<Core dimension> "," <Core dimension list>
<Core dimension> ::= <Dimension name> <Dimension modifier>
<Dimension name> ::= valid Python variable name | valid integer
<Dimension modifier> ::= nil | "?"
注:
-
所有引用只是为了清晰起见。
-
具有相同名称的未修改的核心维度必须具有相同的大小。每个维度名称通常对应于基本函数实现中的一个循环级别。
-
空格将被忽略。
-
作为维度名称的整数会将该维度冻结为特定的值。
-
如果名称后缀有“?”修饰符,则该维度仅在存在于共享它的所有输入和输出上时才是核心维度;否则,它会被忽略(并用一个具有大小为 1 的维度替换基本函数)。
下面是一些签名的示例:
name | signature | common usage |
---|---|---|
add | (),()->() |
二元 ufunc。 |
sum1d | (i)->() |
归约。 |
inner1d | (i),(i)->() |
向量-向量乘法。 |
matmat | (m,n),(n,p)->(m,p) |
矩阵乘法。 |
vecmat | (n),(n,p)->(p) |
向量-矩阵乘法。 |
matvec | (m,n),(n)->(m) |
矩阵-向量乘法。 |
matmul | (m?,n),(n,p?)->(m?,p?) |
以上四种的组合。 |
outer_inner | (i,t),(j,t)->(i,j) |
最后一个维度内积,倒数第二个维度外积,并在其余维度上进行循环/广播。 |
cross1d | (3),(3)->(3) |
交叉积,其中最后一个维度是固定的,必须为 3。 |
最后一个示例是一个核心维度的冻结实例,可用于提高 ufunc 的性能。
用于实现基本函数的 C-API。
当前接口保持不变,仍然可以使用PyUFunc_FromFuncAndData
来实现(专用的)ufunc,由标量基本函数组成。
可以使用PyUFunc_FromFuncAndDataAndSignature
来声明更通用的 ufunc。参数列表与PyUFunc_FromFuncAndData
相同,额外增加了一个参数来指定 C 字符串形式的签名。
此外,回调函数的类型与之前相同,即void (*foo)(char **args, intp *dimensions, intp *steps, void *func)
。调用时,args
是一个长度为nargs
的列表,包含所有输入/输出参数的数据。对于标量基本函数,steps
的长度也是nargs
,表示用于参数的步幅。dimensions
是一个指向定义要循环的轴大小的单个整数的指针。
对于非平凡的签名,dimensions
还将包含核心维度的大小,从第二个条目开始。对于每个唯一的维度名称,只提供一个大小,并且根据签名中维度名称的首次出现给出大小。
steps
的前 nargs
元素与标量 ufunc 相同。接下来的元素按顺序包含所有参数的所有核心维度的步幅。
例如,考虑一个带有签名(i,j),(i)->()
的 ufunc。在这种情况下,args
将包含指向输入/输出数组 a
、b
、c
数据的三个指针。此外,dimensions
将是[N, I, J]
,定义了循环的大小 N
和核心维度 i
和 j
的大小 I
和 J
。最后,steps
将是[a_N, b_N, c_N, a_i, a_j, b_i]
,包含所有必要的步幅。
NumPy 核心库
从 numpy 1.3.0 开始,我们正在致力于将纯 C 的“计算”代码与依赖于 Python 的代码分离。目标是使代码更清洁,并使其能够被 numpy 之外的其他扩展(例如 scipy 等)重用。
NumPy 核心数学库
numpy 核心数学库(‘npymath’)是这一方向的第一步。该库包含大多数与数学相关的 C99 功能,可用于 C99 支持不佳的平台。核心数学函数与 C99 函数具有相同的 API,除了npy_
前缀。
可用的函数在<numpy/npy_math.h>
中定义,疑惑时请参考该头文件。
注意
正在进行努力使npymath
更小(因为随着时间的推移,编译器对 C99 的兼容性已经提高),并且更容易供应商化或作为仅头文件依赖。这将避免使用与下游包或最终用户使用的编译器不匹配的静态库的问题。详细信息,请参阅gh-20880。
浮点数分类
NPY_NAN
此宏定义为 NaN(非数),保证符号位未设置(“正”NaN)。相应的单精度和扩展精度宏可分别添加 F 和 L 后缀获得。
NPY_INFINITY
此宏定义为正无穷大。相应的单精度和扩展精度宏可分别添加 F 和 L 后缀获得。
NPY_PZERO
此宏定义为正零。相应的单精度和扩展精度宏可添加 F 和 L 后缀获得。
NPY_NZERO
此宏定义为负零(即符号位设置)。相应的单精度和扩展精度宏可分别添加 F 和 L 后缀获得。
npy_isnan(x)
此为 C99 isnan 的别名:适用于单精度、双精度和扩展精度,如果 x 为 NaN,则返回非 0 值。
npy_isfinite(x)
此为 C99 isfinite 的别名:适用于单精度、双精度和扩展精度,如果 x 既不是 NaN 也不是无穷大,则返回非 0 值。
npy_isinf(x)
此为 C99 isinf 的别名:适用于单精度、双精度和扩展精度,如果 x 为无穷大(正负均可),则返回非 0 值。
npy_signbit(x)
此为 C99 signbit 的别名:适用于单精度、双精度和扩展精度,如果 x 的符号位设置(即数为负),则返回非 0 值。
npy_copysign(x, y)
此为 C99 copysign 的别名:将 x 的符号设置为 y 的符号。适用于任何值,包括 inf 和 nan。单精度和扩展精度可添加 f 和 l 后缀。
有用的数学常量
下列数学常量可在npy_math.h
中找到。添加f
和l
后缀即可获得单精度和扩展精度。
NPY_E
自然对数的底数((e))
NPY_LOG2E
自然常数的以 2 为底的对数((\frac{\ln(e)}{\ln(2)}))
NPY_LOG10E
自然常数的以 10 为底的对数((\frac{\ln(e)}{\ln(10)}))
NPY_LOGE2
2 的自然对数 ((\ln(2)))
NPY_LOGE10
10 的自然对数 ((\ln(10)))
NPY_PI
π ((\pi))
NPY_PI_2
π除以 2 ((\frac{\pi}{2}))
NPY_PI_4
π除以 4 ((\frac{\pi}{4}))
NPY_1_PI
π的倒数 ((\frac{1}{\pi}))
NPY_2_PI
两倍的π的倒数 ((\frac{2}{\pi}))
NPY_EULER
欧拉常数
(\lim_{n\rightarrow\infty}({\sum_{k=1}^n{\frac{1}{k}}-\ln n}))
低级别的浮点操作
这些在精确的浮点比较中很有用。
double npy_nextafter(double x, double y)
这是 C99 nextafter 的别名:返回 x 方向为 y 的下一个可表示的浮点值。单精度和扩展精度可使用 f 和 l 后缀。
double npy_spacing(double x)
这是一个等价于 Fortran 内在函数的函数。返回 x 和下一个可表示的浮点值之间的距离,例如,spacing(1) == eps。 nan 和+/- inf 的间距返回 nan。单精度和扩展精度可使用 f 和 l 后缀。
void npy_set_floatstatus_divbyzero()
设置除零浮点异常
void npy_set_floatstatus_overflow()
设置浮点溢出异常
void npy_set_floatstatus_underflow()
设置下溢浮点异常
void npy_set_floatstatus_invalid()
设置无效的浮点异常
int npy_get_floatstatus()
获取浮点状态。返回一个具有以下可能标志的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
请注意,npy_get_floatstatus_barrier
更可取,因为它可以防止激进的编译器优化重新排列调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_get_floatstatus_barrier(char*)
获取浮点状态。会传递一个指向本地变量的指针,以防止激进的编译器优化重新排列此函数调用相对于设置状态的代码,这可能导致不正确的结果。
返回一个具有以下可能标志的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
新版本为 1.15.0。
int npy_clear_floatstatus()
清除浮点状态。返回先前的状态掩码。
请注意,npy_clear_floatstatus_barrier
更可取,因为它可以防止激进的编译器优化重新排列调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_clear_floatstatus_barrier(char*)
清除浮点状态。会传递一个指向本地变量的指针,以防止激进的编译器优化重新排列此函数调用相对于设置状态的代码。返回先前的状态掩码。
新版本为 1.15.0。
复数函数
添加了类似于 C99 的复数函数。 如果您希望实现可移植的 C 扩展,则可以使用这些。 由于我们仍然支持不支持 C99 复数类型的平台(最重要的是 Windows,到 2022 年 11 月,MSVC 不支持 C99 复数类型),因此您需要限制为 C90 兼容的语法,例如:
/* a = 1 + 2i \*/
npy_complex a = npy_cpack(1, 2);
npy_complex b;
b = npy_log(a);
在扩展中链接核心数学库
要在您自己的 Python 扩展中使用 NumPy 作为静态库提供的核心数学库,您需要向您的扩展添加 npymath 编译和链接选项。采取的确切步骤将取决于您使用的构建系统。采取的通用步骤包括:
-
将 numpy 包含目录(即
np.get_include()
的值)添加到您的包含目录中, -
npymath
静态库位于紧邻 numpy 包含目录的lib
目录中(即pathlib.Path(np.get_include()) / '..' / 'lib'
)。将其添加到您的库搜索目录中, -
链接到
libnpymath
和libm
。
注
请记住,当您进行交叉编译时,必须使用适用于您构建的平台的numpy
,而不是构建机器的本机平台。否则,您会选择错误架构的静态库。
当您使用numpy.distutils
(已弃用)进行构建时,请在您的setup.py
中使用:
>>> from numpy.distutils.misc_util import get_info
>>> info = get_info('npymath')
>>> _ = config.add_extension('foo', sources=['foo.c'], extra_info=info)
换句话说,使用info
的方式与使用blas_info
等完全相同。
当您使用Meson进行构建时,请使用:
# Note that this will get easier in the future, when Meson has
# support for numpy built in; most of this can then be replaced
# by `dependency('numpy')`.
incdir_numpy = run_command(py3,
[
'-c',
'import os; os.chdir(".."); import numpy; print(numpy.get_include())'
],
check: true
).stdout().strip()
inc_np = include_directories(incdir_numpy)
cc = meson.get_compiler('c')
npymath_path = incdir_numpy / '..' / 'lib'
npymath_lib = cc.find_library('npymath', dirs: npymath_path)
py3.extension_module('module_name',
...
include_directories: inc_np,
dependencies: [npymath_lib],
半精度函数
头文件<numpy/halffloat.h>
提供了用于处理 IEEE 754-2008 16 位浮点值的函数。虽然这种格式通常不用于数值计算,但对于存储需要浮点但不需要太多精度的值是很有用的。它也可以用作理解浮点舍入误差本质的教育工具。
与其他类型一样,NumPy 包含一个用于 16 位浮点数的 npy_half 的 typedef。与大多数其他类型不同,您不能在 C 中将其用作普通类型,因为它是 npy_uint16 的 typedef。例如,1.0 在 C 中看起来像 0x3c00,如果您在不同的带符号零之间进行相等比较,您会得到-0.0 != 0.0(0x8000 != 0x0000),这是不正确的。
出于这些原因,NumPy 提供了一个 API 来处理通过包括<numpy/halffloat.h>
和链接到npymath
可访问的 npy_half 值。对于没有直接提供的函数,如算术运算,首选方法是转换为 float 或 double,然后再次转换为 half,就像以下示例一样。
npy_half sum(int n, npy_half *array) {
float ret = 0;
while(n--) {
ret += npy_half_to_float(*array++);
}
return npy_float_to_half(ret);
}
外部链接:
NPY_HALF_ZERO
这个宏被定义为正零。
NPY_HALF_PZERO
这个宏被定义为正零。
NPY_HALF_NZERO
这个宏被定义为负零。
NPY_HALF_ONE
这个宏被定义为 1.0。
NPY_HALF_NEGONE
这个宏被定义为-1.0。
NPY_HALF_PINF
这个宏被定义为正无穷。
NPY_HALF_NINF
这个宏被定义为负无穷。
NPY_HALF_NAN
这个宏被定义为 NaN 值,保证其符号位未设置。
float npy_half_to_float( h)
将半精度浮点数转换为单精度浮点数。
double npy_half_to_double( h)
将半精度浮点数转换为双精度浮点数。
npy_float_to_half(float f)
将单精度浮点数转换为半精度浮点数。值四舍五入为最接近的可表示的一半,平局取最近的偶数。如果值太小或者太大,系统的浮点下溢或上溢位将被设置。
npy_double_to_half(double d)
将双精度浮点数转换为半精度浮点数。值四舍五入到最接近的可表示的一半,平局取最近的偶数。如果值太小或太大,系统的浮点下溢或上溢位将被设置。
int npy_half_eq( h1, h2)
比较两个半精度浮点数(h1 == h2)。
int npy_half_ne( h1, h2)
比较两个半精度浮点数(h1 != h2)。
int npy_half_le( h1, h2)
比较两个半精度浮点数(h1 <= h2)。
int npy_half_lt( h1, h2)
比较两个半精度浮点数(h1 < h2)。
int npy_half_ge( h1, h2)
比较两个半精度浮点数(h1 >= h2)。
int npy_half_gt( h1, h2)
比较两个半精度浮点数(h1 > h2)。
int npy_half_eq_nonan( h1, h2)
比较两个已知不是 NaN 的半精度浮点数(h1 == h2)。如果值是 NaN,则结果未定义。
int npy_half_lt_nonan( h1, h2)
比较两个已知不是 NaN 的半精度浮点数(h1 < h2)。如果值是 NaN,则结果未定义。
int npy_half_le_nonan( h1, h2)
比较两个已知不是 NaN 的半精度浮点数(h1 <= h2)。如果值是 NaN,则结果未定义。
int npy_half_iszero( h)
测试半精度浮点数是否值为零。这可能比调用 npy_half_eq(h, NPY_ZERO)要稍快。
int npy_half_isnan( h)
测试半精度浮点数是否是 NaN。
int npy_half_isinf( h)
测试半精度浮点数是否是正或负无穷大。
int npy_half_isfinite( h)
测试半精度浮点数是否是有限的(不是 NaN 或 Inf)。
int npy_half_signbit( h)
返回 1,如果 h 是负数,否则返回 0。
npy_half_copysign( x, y)
返回具有从 y 复制的符号位的 x 的值。适用于任何值,包括 Inf 和 NaN。
npy_half_spacing( h)
这与低级浮点部分中描述的 npy_spacing 和 npy_spacingf 对于半精度浮点数是相同的。
npy_half_nextafter( x, y)
这与低级浮点部分中描述的 npy_nextafter 和 npy_nextafterf 对于半精度浮点数是相同的。
npy_floatbits_to_halfbits( f)
低级函数,将 32 位单精度浮点数,存储为 uint32,转换为 16 位半精度浮点数。
npy_doublebits_to_halfbits( d)
低级函数,将 64 位双精度浮点数,存储为 uint64,转换为 16 位半精度浮点数。
npy_halfbits_to_floatbits( h)
低级函数,将一个 16 位半精度浮点数转换为 32 位单精度浮点数,存储为 uint32。
npy_halfbits_to_doublebits( h)
将一个 16 位半精度浮点数转换为 64 位双精度浮点数,存储为 uint64 的低级函数。
NumPy 核心数学库
Numpy 核心数学库(‘npymath’)是朝这个方向迈出的第一步。该库包含大多数与数学相关的 C99 功能,可用于 C99 支持不佳的平台。核心数学函数的 API 与 C99 函数相同,除了npy_*
前缀。
可用函数在<numpy/npy_math.h>
中定义-当有疑问时,请参考此头文件。
注意
现在正努力使 npymath
更小(因为随着时间的推移,编译器的 C99 兼容性已经得到改善),并且更容易供应商使用或作为头文件依赖。这将避免使用与下游包或最终用户所用编译器不匹配的静态库的发货问题。有关详细信息,请参见 gh-20880。
浮点数分类
NPY_NAN
此宏被定义为 NaN(不是一个数),并且保证符号位未设置('正' NaN)。相应的单精度和扩展精度宏可用后缀 F 和 L。
NPY_INFINITY
此宏被定义为正无穷。相应的单精度和扩展精度宏可用后缀 F 和 L。
NPY_PZERO
此宏被定义为正零。相应的单精度和扩展精度宏可用后缀 F 和 L。
NPY_NZERO
此宏被定义为负零(即符号位设置为 1)。相应的单精度和扩展精度宏可用后缀 F 和 L。
npy_isnan(x)
这是 C99 的 isnan 的别名:适用于单精度、双精度和扩展精度,并在 x 是 NaN 时返回非零值。
npy_isfinite(x)
这是 C99 的 isfinite 的别名:适用于单精度、双精度和扩展精度,并且在 x 既不是 NaN 也不是无穷时返回非零值。
npy_isinf(x)
这是 C99 的 isinf 的别名:适用于单精度、双精度和扩展精度,并且在 x 是无穷(正无穷和负无穷)时返回非零值。
npy_signbit(x)
这是 C99 的 signbit 的别名:适用于单精度、双精度和扩展精度,并且在 x 的符号位设置时返回非零值(即数是负数)。
npy_copysign(x, y)
这是 C99 的 copysign 的别名:返回与 y 相同符号的 x。适用于任何值,包括 inf 和 nan。单精度和扩展精度可用后缀 f 和 l。
有用的数学常数
以下数学常数可在 npy_math.h
中使用。单精度和扩展精度也可通过添加 f
和 l
后缀分别使用。
NPY_E
自然对数的底数 ((e))
NPY_LOG2E
欧拉常数的以 2 为底的对数 ((\frac{\ln(e)}{\ln(2)}))
NPY_LOG10E
欧拉常数以 10 为底的对数 ((\frac{\ln(e)}{\ln(10)}))
NPY_LOGE2
自然对数的 2 ((\ln(2)))
NPY_LOGE10
自然对数的 10 ((\ln(10)))
NPY_PI
圆周率 ((\pi))
NPY_PI_2
Pi 除以 2 ((\frac{\pi}{2}))
NPY_PI_4
Pi 除以 4 ((\frac{\pi}{4}))
NPY_1_PI
Pi 的倒数 ((\frac{1}{\pi}))
NPY_2_PI
2 乘以 pi 的倒数 ((\frac{2}{\pi}))
NPY_EULER
欧拉常数
(\lim_{n\rightarrow\infty}({\sum_{k=1}^n{\frac{1}{k}}-\ln n}))
低级浮点数操作
这些对精确的浮点数比较很有用。
double npy_nextafter(double x, double y)
这是对 C99 的 nextafter 的别名:返回从 x 向 y 方向的下一个可表示浮点值。后缀 f 和 l 可用于单精度和扩展精度。
double npy_spacing(double x)
这是等效于 Fortran 内置函数的函数。返回与 x 和下一个可表示的浮点值之间的距离,例如,spacing(1) == eps。 nan 和+/- inf 的间距返回 nan。 带有后缀 f 和 l 的单精度和扩展精度可用。
void npy_set_floatstatus_divbyzero()
设置除零浮点异常
void npy_set_floatstatus_overflow()
设置溢出的浮点异常
void npy_set_floatstatus_underflow()
设置下溢的浮点异常
void npy_set_floatstatus_invalid()
设置无效的浮点异常
int npy_get_floatstatus()
获取浮点状态。返回具有以下可能标志的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
请注意,npy_get_floatstatus_barrier
更可取,因为它可以防止激进的编译器优化重新排列调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_get_floatstatus_barrier(char*)
获取浮点状态。传递到本地变量的指针可防止激进的编译器优化重新排列此函数调用相对于设置状态的代码,这可能导致不正确的结果。
返回具有以下可能标志的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
版本 1.15.0 中新增。
int npy_clear_floatstatus()
清除浮点状态。返回以前的状态掩码。
请注意,npy_clear_floatstatus_barrier
更可取,因为它可以防止激进的编译器优化重新排列调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_clear_floatstatus_barrier(char*)
清除浮点状态。传递给本地变量的指针可以防止激进的编译器优化重新排列此函数调用。返回以前的状态掩码。
版本 1.15.0 中新增。
复杂函数
添加了类似 C99 的复杂函数。如果您希望实现可移植的 C 扩展程序,则可以使用这些。由于我们仍然支持没有 C99 复杂类型的平台(最重要的是 Windows,在那里,截至 2022 年 11 月,MSVC 不支持 C99 复杂类型),您需要限制为 C90 兼容语法,例如:
/* a = 1 + 2i \*/
npy_complex a = npy_cpack(1, 2);
npy_complex b;
b = npy_log(a);
在扩展中链接核心数学库
要在自己的 Python 扩展中使用 NumPy 提供的核心数学库作为静态库,您需要将 npymath 编译和链接选项添加到您的扩展程序中。要采取的确切步骤将取决于您正在使用的构建系统。要采取的通用步骤包括:
-
将 numpy 包括目录(=
np.get_include()
的值)添加到您的包括目录中, -
npymath
静态库位于紧挨着 numpy 包括目录的lib
目录中(即,pathlib.Path(np.get_include()) / '..' / 'lib'
)。将其添加到您的库搜索目录中, -
使用
libnpymath
和libm
进行链接。
注意
请记住,在交叉编译时,必须使用适用于您要构建的平台的numpy
,而不是适用于构建机器的本机平台的numpy
。否则,您将获得为错误架构构建的静态库。
使用numpy.distutils
(不推荐)进行构建时,在您的setup.py
中使用:
>>> from numpy.distutils.misc_util import get_info
>>> info = get_info('npymath')
>>> _ = config.add_extension('foo', sources=['foo.c'], extra_info=info)
换句话说,使用info
的方式与使用blas_info
等完全相同。
在构建时使用Meson,使用:
# Note that this will get easier in the future, when Meson has
# support for numpy built in; most of this can then be replaced
# by `dependency('numpy')`.
incdir_numpy = run_command(py3,
[
'-c',
'import os; os.chdir(".."); import numpy; print(numpy.get_include())'
],
check: true
).stdout().strip()
inc_np = include_directories(incdir_numpy)
cc = meson.get_compiler('c')
npymath_path = incdir_numpy / '..' / 'lib'
npymath_lib = cc.find_library('npymath', dirs: npymath_path)
py3.extension_module('module_name',
...
include_directories: inc_np,
dependencies: [npymath_lib],
半精度函数
头文件<numpy/halffloat.h>
提供了处理 IEEE 754-2008 16 位浮点值的函数。虽然此格式通常不用于数值计算,但对于存储不需要太多精度的值很有用。 它还可以用作理解浮点数舍入误差性质的教育工具。
与其他类型一样,NumPy 包括一个用于 16 位浮点数的 typedef npy_half。与大多数其他类型不同,您不能在 C 中将其用作正常类型,因为它是 npy_uint16 的 typedef。 例如,1.0 在 C 中看起来像 0x3c00,如果您在不同的有符号零之间进行相等比较,您将得到-0.0 != 0.0(0x8000 != 0x0000),这是不正确的。
出于这些原因,NumPy 提供了一个 API 来处理通过包含<numpy/halffloat.h>
并链接到npymath
访问的 npy_half 值。 对于直接提供的函数,如算术运算,优选方法是转换为 float 或 double,然后再次转换,如下例所示。
npy_half sum(int n, npy_half *array) {
float ret = 0;
while(n--) {
ret += npy_half_to_float(*array++);
}
return npy_float_to_half(ret);
}
外部链接:
NPY_HALF_ZERO
宏定义为正零。
NPY_HALF_PZERO
宏定义为正零。
NPY_HALF_NZERO
宏定义为负零。
NPY_HALF_ONE
宏定义为 1.0。
NPY_HALF_NEGONE
宏定义为-1.0。
NPY_HALF_PINF
宏定义为+inf。
NPY_HALF_NINF
宏定义为-inf。
NPY_HALF_NAN
宏定义为 NaN 值,保证其符号位未设置。
float npy_half_to_float( h)
将半精度浮点数转换为单精度浮点数。
double npy_half_to_double( h)
将半精度浮点数转换为双精度浮点数。
npy_float_to_half(float f)
将单精度浮点数转换为半精度浮点数。该值四舍五入为最接近的可表示的半精度数,如果太小或太大,则系统的浮点下溢或溢出位将被设置。
npy_double_to_half(double d)
将双精度浮点数转换为半精度浮点数。该值四舍五入为最接近的可表示的半精度数,如果太小或太大,则系统的浮点下溢或溢出位将被设置。
int npy_half_eq( h1, h2)
比较两个半精度浮点数(h1 == h2)。
int npy_half_ne( h1, h2)
比较两个半精度浮点数(h1 != h2)。
int npy_half_le( h1, h2)
比较两个半精度浮点数(h1 <= h2)。
int npy_half_lt( h1, h2)
比较两个半精度浮点数(h1 < h2)。
int npy_half_ge( h1, h2)
比较两个半精度浮点数(h1 >= h2)。
int npy_half_gt( h1, h2)
比较两个半精度浮点数(h1 > h2)。
int npy_half_eq_nonan( h1, h2)
比较已知不是 NaN 的两个半精度浮点数(h1 == h2)。如果值为 NaN,则结果是未定义的。
int npy_half_lt_nonan( h1, h2)
比较已知不是 NaN 的两个半精度浮点数(h1 < h2)。如果值为 NaN,则结果是未定义的。
int npy_half_le_nonan( h1, h2)
比较已知不是 NaN 的两个半精度浮点数(h1 <= h2)。如果值为 NaN,则结果是未定义的。
int npy_half_iszero( h)
测试半精度浮点数是否等于零。这可能比调用 npy_half_eq(h, NPY_ZERO) 更快。
int npy_half_isnan( h)
测试半精度浮点数是否为 NaN。
int npy_half_isinf( h)
测试半精度浮点数是否为正负无穷。
int npy_half_isfinite( h)
测试半精度浮点数是否有限(非 NaN 或 Inf)。
int npy_half_signbit( h)
如果 h 为负,则返回 1,否则返回 0。
npy_half_copysign( x, y)
返回从 y 复制的符号位的 x 值。适用于任何值,包括 Inf 和 NaN。
npy_half_spacing( h)
这与低级浮点部分中描述的 npy_spacing 和 npy_spacingf 类似。
npy_half_nextafter( x, y)
这与低级浮点部分中描述的 npy_nextafter 和 npy_nextafterf 类似,用于半精度浮点数。
npy_floatbits_to_halfbits( f)
将以 uint32 存储的 32 位单精度浮点数转换为 16 位半精度浮点数的低级功能。
npy_doublebits_to_halfbits( d)
将以 uint64 存储的 64 位双精度浮点数转换为 16 位半精度浮点数的低级功能。
npy_halfbits_to_floatbits( h)
将 16 位半精度浮点数转换为以 uint32 存储的 32 位单精度浮点数的低级功能。
npy_halfbits_to_doublebits( h)
将 16 位半精度浮点数转换为以 uint64 存储的 64 位双精度浮点数的低级功能。
浮点分类
NPY_NAN
此宏被定义为 NaN(非数字),并保证符号位未设置('正' NaN)。相应的单精度和扩展精度宏可在后缀 F 和 L 中使用。
NPY_INFINITY
此宏被定义为正无穷。相应的单精度和扩展精度宏可在后缀 F 和 L 中使用。
NPY_PZERO
此宏被定义为正零。相应的单精度和扩展精度宏可在后缀 F 和 L 中使用。
NPY_NZERO
此宏被定义为负零(即符号位设置)。相应的单精度和扩展精度宏可在后缀 F 和 L 中使用。
npy_isnan(x)
这是 C99 isnan 的别名:适用于单精度、双精度和扩展精度,并在 x 为 NaN 时返回非零值。
npy_isfinite(x)
这是 C99 isfinite 的别名:适用于单精度、双精度和扩展精度,并在 x 既非 NaN 也非无穷时返回非零值。
npy_isinf(x)
这是 C99 isinf 的别名:适用于单精度、双精度和扩展精度,并在 x 是无限值(正负)时返回非零值。
npy_signbit(x)
这是 C99 signbit 的别名:适用于单精度、双精度和扩展精度,并在 x 有符号位设置时(即数字为负)返回非零值。
npy_copysign(x, y)
这是 C99 copysign 的别名:返回与 y 相同符号的 x。适用于任何值,包括 inf 和 nan。单精度和扩展精度可在后缀 f 和 l 中使用。
有用的数学常量
以下数学常量在npy_math.h
中可用。也可以通过添加f
和l
后缀来使用单精度和扩展精度。
NPY_E
自然对数的底((e))
NPY_LOG2E
欧拉常数的以 2 为底的对数((\frac{\ln(e)}{\ln(2)}))
NPY_LOG10E
欧拉常数的以 10 为底的对数((\frac{\ln(e)}{\ln(10)}))
NPY_LOGE2
自然对数 2((\ln(2)))
NPY_LOGE10
自然对数 10((\ln(10)))
NPY_PI
π((\pi))
NPY_PI_2
π除以 2((\frac{\pi}{2}))
NPY_PI_4
常数π除以 4((\frac{\pi}{4}))
NPY_1_PI
π的倒数((\frac{1}{\pi}))
NPY_2_PI
π的倒数的两倍((\frac{2}{\pi}))
NPY_EULER
欧拉常数
(\lim_{n\rightarrow\infty}({\sum_{k=1}^n{\frac{1}{k}}-\ln n}))
低级浮点操作
这些对于精确的浮点比较可能很有用。
double npy_nextafter(double x, double y)
这是 C99 的 nextafter 的别名:返回从 x 到 y 方向的下一个可表示的浮点值。单精度和扩展精度可用后缀 f 和 l。
double npy_spacing(double x)
这是一个等效于 Fortran 内在函数的函数。返回 x 和 x 的下一个可表示的浮点值之间的距离,例如 spacing(1) == eps。 nan 和+/- inf 的间距返回 nan。单精度和扩展精度可用后缀 f 和 l。
void npy_set_floatstatus_divbyzero()
设置除以零的浮点异常
void npy_set_floatstatus_overflow()
设置上溢的浮点异常
void npy_set_floatstatus_underflow()
设置下溢的浮点异常
void npy_set_floatstatus_invalid()
设置无效的浮点异常
int npy_get_floatstatus()
获取浮点状态。返回具有以下可能标志位的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
请注意,npy_get_floatstatus_barrier
更可取,因为它会防止编译器优化重新安排调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_get_floatstatus_barrier(char*)
获取浮点状态。传入一个本地变量的指针以防止过于激进的编译器优化重新安排此函数调用以相对于设置状态的代码,这可能导致不正确的结果。
返回具有以下可能标志位的位掩码:
-
NPY_FPE_DIVIDEBYZERO
-
NPY_FPE_OVERFLOW
-
NPY_FPE_UNDERFLOW
-
NPY_FPE_INVALID
在版本 1.15.0 中新增。
int npy_clear_floatstatus()
清除浮点状态。返回先前的状态掩码。
请注意,npy_clear_floatstatus_barrier
更可取,因为它会防止编译器优化重新安排调用相对于设置状态的代码,这可能导致不正确的结果。
int npy_clear_floatstatus_barrier(char*)
清除浮点状态。传入一个本地变量的指针以防止过于激进的编译器优化重新安排此函数调用。返回先前的状态掩码。
在版本 1.15.0 中新增。
复杂函数
已添加类似于 C99 的复数函数。如果你希望实现可移植的 C 扩展,可以使用这些函数。由于我们仍然支持没有 C99 复数类型的平台(最重要的是 Windows,截至 2022 年 11 月,MSVC 不支持 C99 复数类型),你需要限制为兼容 C90 的语法,例如:
/* a = 1 + 2i \*/
npy_complex a = npy_cpack(1, 2);
npy_complex b;
b = npy_log(a);
在扩展中链接核心数学库
要在你自己的 Python 扩展中使用 NumPy 提供的核心数学库作为静态库,你需要为你的扩展添加npymath
编译和链接选项。具体的步骤将取决于你使用的构建系统。一般的步骤如下:
-
将 numpy 的包含目录(即
np.get_include()
的值)添加到你的包含目录中, -
npymath
静态库位于 numpy 的包含目录旁边的lib
目录中(即pathlib.Path(np.get_include()) / '..' / 'lib'
)。将其添加到你的库搜索目录中, -
链接时使用
libnpymath
和libm
。
注意
请记住,当你交叉编译时,你必须使用与你构建的平台相关的numpy
,而不是构建机器上本地的平台。否则,你会选择错误架构的静态库。
当你使用numpy.distutils
(不推荐使用)构建时,在你的setup.py
中使用以下代码:
>>> from numpy.distutils.misc_util import get_info
>>> info = get_info('npymath')
>>> _ = config.add_extension('foo', sources=['foo.c'], extra_info=info)
换句话说,使用info
的方式与使用blas_info
等是完全相同的。
当你使用Meson构建时,使用以下代码:
# Note that this will get easier in the future, when Meson has
# support for numpy built in; most of this can then be replaced
# by `dependency('numpy')`.
incdir_numpy = run_command(py3,
[
'-c',
'import os; os.chdir(".."); import numpy; print(numpy.get_include())'
],
check: true
).stdout().strip()
inc_np = include_directories(incdir_numpy)
cc = meson.get_compiler('c')
npymath_path = incdir_numpy / '..' / 'lib'
npymath_lib = cc.find_library('npymath', dirs: npymath_path)
py3.extension_module('module_name',
...
include_directories: inc_np,
dependencies: [npymath_lib],
半精度函数
头文件<numpy/halffloat.h>
提供了与 IEEE 754-2008 16 位浮点数值一起工作的函数。虽然这种格式通常不用于数值计算,但它非常适合存储需要浮点数但不需要太高精度的值。它也可以用作理解浮点数舍入误差性质的教育工具。
像其他类型一样,NumPy 包括一个npy_half
的类型定义,用于表示 16 位浮点数。与大多数其他类型不同,你不能在 C 中将它用作普通类型,因为它是npy_uint16
的一个类型定义。例如,对于 C 来说,1.0 看起来像 0x3c00,如果你在不同的有符号零之间进行相等比较,你将得到-0.0 != 0.0 (0x8000 != 0x0000),这是不正确的。
出于这些原因,NumPy 提供了一个 API 来处理通过包含<numpy/halffloat.h>
和链接到npymath
可访问的npy_half
值。对于那些没有直接提供的函数,例如算术运算,首选方法是先转换为浮点数或双精度数,然后再转换回来,如下面的例子所示。
npy_half sum(int n, npy_half *array) {
float ret = 0;
while(n--) {
ret += npy_half_to_float(*array++);
}
return npy_float_to_half(ret);
}
外部链接:
NPY_HALF_ZERO
此宏被定义为正零。
NPY_HALF_PZERO
此宏被定义为正零。
NPY_HALF_NZERO
此宏被定义为负零。
NPY_HALF_ONE
此宏被定义为 1.0。
NPY_HALF_NEGONE
此宏被定义为-1.0。
NPY_HALF_PINF
此宏被定义为+inf。
NPY_HALF_NINF
此宏被定义为-inf。
NPY_HALF_NAN
此宏被定义为 NaN 值,保证其符号位未设置。
float npy_half_to_float( h)
将半精度浮点数转换为单精度浮点数。
double npy_half_to_double( h)
将半精度浮点数转换为双精度浮点数。
npy_float_to_half(float f)
将单精度浮点数转换为半精度浮点数。该值四舍五入为最接近的可表示的一半,如果值太小或太大,则系统的浮点下溢位或上溢位将被设置。
npy_double_to_half(double d)
将双精度浮点数转换为半精度浮点数。该值四舍五入为最接近的可表示的一半,如果值太小或太大,则系统的浮点下溢位或上溢位将被设置。
int npy_half_eq( h1, h2)
比较两个半精度浮点数(h1 == h2)。
int npy_half_ne( h1, h2)
比较两个半精度浮点数(h1 != h2)。
int npy_half_le( h1, h2)
比较两个半精度浮点数(h1 <= h2)。
int npy_half_lt( h1, h2)
比较两个半精度浮点数(h1 < h2)。
int npy_half_ge( h1, h2)
比较两个半精度浮点数(h1 >= h2)。
int npy_half_gt( h1, h2)
比较两个半精度浮点数(h1 > h2)。
int npy_half_eq_nonan( h1, h2)
比较已知不为 NaN 的两个半精度浮点数(h1 == h2)。如果一个值是 NaN,则结果是未定义的。
int npy_half_lt_nonan( h1, h2)
比较已知不为 NaN 的两个半精度浮点数(h1 < h2)。如果一个值是 NaN,则结果是未定义的。
int npy_half_le_nonan( h1, h2)
比较已知不为 NaN 的两个半精度浮点数(h1 <= h2)。如果一个值是 NaN,则结果是未定义的。
int npy_half_iszero( h)
检测半精度浮点数是否具有等于零的值。这可能比调用 npy_half_eq(h, NPY_ZERO)稍微快一些。
int npy_half_isnan( h)
检测半精度浮点数是否为 NaN。
int npy_half_isinf( h)
检测半精度浮点数是否为正或负 Inf。
int npy_half_isfinite( h)
检测半精度浮点数是否有限(不是 NaN 或 Inf)。
int npy_half_signbit( h)
如果 h 为负则返回 1,否则返回 0。
npy_half_copysign( x, y)
将 x 的符号位从 y 中复制的值返回。适用于任何值,包括 Inf 和 NaN。
npy_half_spacing( h)
这与低级浮点部分中描述的 npy_spacing 和 npy_spacingf 对于半精度浮点数是相同的。
npy_half_nextafter( x, y)
这与低级浮点部分中描述的 npy_nextafter 和 npy_nextafterf 对于半精度浮点数是相同的。
npy_floatbits_to_halfbits( f)
将 32 位单精度浮点数,存储为 uint32,转换为 16 位半精度浮点数的低级函数。
npy_doublebits_to_halfbits( d)
将 64 位双精度浮点数,存储为 uint64,转换为 16 位半精度浮点数的低级函数。
npy_halfbits_to_floatbits( h)
将 16 位半精度浮点数转换为 32 位单精度浮点数(存储为 uint32)的低级函数。
npy_halfbits_to_doublebits( h)
将 16 位半精度浮点数转换为 64 位双精度浮点数(存储为 uint64)的低级函数。
C API 弃用
背景
多年来,NumPy 为第三方扩展暴露的 API 已发展壮大,并使程序员能够直接从 C 中访问 NumPy 功能。这个 API 最好被描述为“有机的”。它是由多种竞争性的愿望和多种观点多年形成的,受到希望使用户能够从 Numeric 和 Numarray 迁移到 NumPy 方面的强烈影响。核心 API 始于 1995 年的 Numeric,并有一些模式,比如大量使用宏来模仿 Python 的 C-API 以及适应 90 年代晚期的编译器技术。只有一小群志愿者很少有时间投入到改进这个 API 上。
目前正在努力改进 API。在这个努力中,重要的是要确保适用于 NumPy 1.X 的代码继续适用于 NumPy 1.X。同时,某些 API 将被标记为弃用,以便能朝着未来的代码避开这些 API,并采用更好的做法。
C API 中弃用标记的另一个重要作用是朝着隐藏 NumPy 实现的内部细节前进。对于那些需要直接、轻松地访问 ndarrays 数据的人来说,这不会移除这种功能。相反,有许多潜在的性能优化需要改变实现细节,并且由于保留 ABI 兼容性的价值很高,NumPy 开发人员现在无法尝试这些优化。通过弃用这种直接访问方法,将来我们将能够以我们目前无法做到的方式提高 NumPy 的性能。
弃用机制 NPY_NO_DEPRECATED_API
在 C 语言中,没有像 Python 那样支持弃用警告需要进行改进的功能。处理弃用的一种方法是在文档和发布说明中标记它们,然后在将来的主要版本(如 NumPy 2.0 及以后)中删除或更改弃用的功能。但 NumPy 的次要版本不应有导致在之前的次要版本上正常运行的代码无法运行的主要 C-API 更改。例如,我们将尽力确保在 NumPy 1.4 上编译和运行的代码应该继续在 NumPy 1.7 上运行(但可能会有编译器警告)。
要使用 NPY_NO_DEPRECATED_API 机制,您需要在#include 任何 NumPy 头文件之前将其#define 为 NumPy 的目标 API 版本。如果您希望确认您的代码对 1.7 干净,在 C 中使用:
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
在支持#warning 机制的编译器上,如果您没有定义符号 NPY_NO_DEPRECATED_API,NumPy 将发出一个编译器警告。这样一来,第三方开发人员可能没有仔细阅读发布说明的事实会被标记为有弃用功能。
请注意,定义 NPY_NO_DEPRECATED_API 并不足以使您的扩展 ABI 与给定的 NumPy 版本兼容。请参阅对下游包作者。
背景
NumPy 为第三方扩展所公开的 API 已经经过多年的版本发布,并允许程序员直接从 C 访问 NumPy 功能。这个 API 最好可以描述为“有机的”。多年来,它已经从多个竞争的愿望和多个观点中出现,并且受到了从 Numeric 和 Numarray 转移到 NumPy 的用户方便的强烈影响。核心 API 最初是由 1995 年的 Numeric 创建的,存在一些模式,例如大量使用的宏,用于模仿 Python 的 C-API,并考虑了 90 年代后期的编译器技术。并且,有一个只有很少时间来改进这个 API 的小团队志愿者。
正在努力改进 API。在这个努力中,确保为 NumPy 1.X 编写的代码仍然可以为 NumPy 1.X 编译非常重要。同时,某些 API 将被标记为弃用,以便未来的代码可以避免使用这些 API,并遵循更好的实践。
C API 中弃用标记扮演的另一个重要角色是朝着隐藏 NumPy 实现的内部细节。对于那些需要直接、简单地访问 ndarrays 数据的人来说,这并不会删除这种能力。相反,有许多潜在的性能优化需要更改实现细节,而目前由于保存 ABI 兼容性的重要性,NumPy 开发人员无法尝试这些优化措施。通过弃用这种直接访问方式,我们将来能够以目前无法实现的方式改进 NumPy 的性能。
弃用机制 NPY_NO_DEPRECATED_API
在 C 中,没有相当于 Python 支持的弃用警告的机制。进行弃用的一种方法是在文档和发布说明中标记它们,然后在将来的主要版本(NumPy 2.0 及以后)中删除或更改已弃用的功能。NumPy 的次要版本不应该有主要的 C-API 更改,这会阻止之前的次要版本上运行的代码。例如,我们将尽力确保在 NumPy 1.4 上编译并运行的代码应该在 NumPy 1.7 上(可能会出现编译器警告)继续运行。
要使用 NPY_NO_DEPRECATED_API 机制,在#include 任何 NumPy 头文件之前,您需要将其定义为 NumPy 的目标 API 版本。如果您想确认您的代码是否适用于 1.7,请使用:
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
在支持#warning 机制的编译器上,如果您没有定义符号 NPY_NO_DEPRECATED_API,NumPy 会发出编译器警告。这样,那些可能没有仔细阅读发布说明的第三方开发人员将会注意到已经弃用的事实。
请注意,仅定义 NPY_NO_DEPRECATED_API 并不足以使您的扩展与特定的 NumPy 版本 ABI 兼容。参见面向下游软件包作者。
NumPy 中的内存管理
numpy.ndarray
是一个 Python 类。它需要额外的内存分配来保存 numpy.ndarray.strides
、numpy.ndarray.shape
和 numpy.ndarray.data
属性。这些属性在创建 Python 对象后在 new 中特别分配。strides
和 shape
存储在内部分配的内存块中。
用于存储实际数组值的 data
分配(在object
数组的情况下可能是指针)可能非常大,因此 NumPy 提供了管理其分配和释放的接口。本文详细介绍了这些接口的工作原理。
历史概述
自版本 1.7.0 起,NumPy 暴露了一组 PyDataMem_*
函数(PyDataMem_NEW
、PyDataMem_FREE
、PyDataMem_RENEW
),分别由 alloc、free、realloc 支持。在该版本中,NumPy 也公开了下面描述的 PyDataMem_EventHook 函数(现已废弃),它封装了 OS 级别的调用。
自那些早期以来,Python 也改进了其内存管理能力,并在 3.4 版本中开始提供各种管理策略。这些例程分为一组域,每个域都有一个用于内存管理的 PyMemAllocatorEx
结构。Python 还添加了一个用于跟踪对各种例程的调用的 tracemalloc
模块。这些跟踪钩子已添加到 NumPy 的 PyDataMem_*
例程中。
NumPy 在其内部的 npy_alloc_cache
、npy_alloc_cache_zero
和 npy_free_cache
函数中添加了一小块已分配内存的缓存。这些函数分别封装了 alloc
、alloc-and-memset(0)
和 free
,但当调用 npy_free_cache
时,它会将指针添加到一个以大小标记的可用块的短列表中。这些块可以被后续对 npy_alloc*
的调用重新使用,避免内存抖动。
NumPy 中的可配置内存例程(NEP 49)
用户可能希望使用自己的内部数据内存例程来覆盖内部的数据内存例程。由于 NumPy 不使用 Python 领域策略来管理数据内存,它提供了一组替代的 C-API 来更改内存例程。对于大块对象数据,没有 Python 领域范围的策略,因此这些不太适合 NumPy 的需求。希望更改 NumPy 数据内存管理例程的用户可以使用 PyDataMem_SetHandler
,它使用一个 PyDataMem_Handler
结构体来保存用于管理数据内存的函数指针。调用仍然由内部例程包装以调用 PyTraceMalloc_Track
,PyTraceMalloc_Untrack
,并将使用已弃用的 PyDataMem_EventHookFunc
机制。由于函数可能在进程的生命周期内发生变化,每个 ndarray
都携带着在其实例化时使用的函数,并且这些函数将用于重新分配或释放该实例的数据内存。
type PyDataMem_Handler
一个用于保存用于操作内存的函数指针的结构体
typedef struct {
char name[127]; /* multiple of 64 to keep the struct aligned */
uint8_t version; /* currently 1 */
PyDataMemAllocator allocator;
} PyDataMem_Handler;
分配器结构体所在位置
/* The declaration of free differs from PyMemAllocatorEx */
typedef struct {
void *ctx;
void* (*malloc) (void *ctx, size_t size);
void* (*calloc) (void *ctx, size_t nelem, size_t elsize);
void* (*realloc) (void *ctx, void *ptr, size_t new_size);
void (*free) (void *ctx, void *ptr, size_t size);
} PyDataMemAllocator;
*PyDataMem_SetHandler( *handler)
设置新的分配策略。如果输入值为NULL
,则将策略重置为默认值。返回先前的策略,如果发生错误则返回NULL
。我们包装用户提供的函数,以便它们依然调用 Python 和 numpy 内存管理回调钩子。
*PyDataMem_GetHandler()
返回将用于为下一个 PyArrayObject
分配数据的当前策略。失败时返回NULL
。
关于设置和使用 PyDataMem_Handler 的示例,请参见 numpy/core/tests/test_mem_policy.py
中的测试
void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data);
在数据内存操作期间将调用此函数
*PyDataMem_SetEventHook( *newhook, void *user_data, void **old_data)
为 numpy 数组数据设置分配事件钩子。
返回指向先前钩子的指针或NULL
。如果 old_data 非NULL
,则将将先前的 user_data 指针复制到其中。
如果非NULL
,钩子将在每个 PyDataMem_NEW/FREE/RENEW
结束时被调用
result = PyDataMem_NEW(size) -> (*hook)(NULL, result, size, user_data)
PyDataMem_FREE(ptr) -> (*hook)(ptr, NULL, 0, user_data)
result = PyDataMem_RENEW(ptr, size) -> (*hook)(ptr, result, size, user_data)
当调用钩子时,全局解释器锁将由调用线程持有。如果执行可能导致新分配事件的操作(例如创建/销毁 numpy 对象,或创建/销毁可能导致垃圾回收的 Python 对象),则钩子应该被编写为可重入。
在 v1.23 中弃用
如果没有设置策略,释放时会发生什么
一种罕见但有用的技术是在 NumPy 之外分配一个缓冲区,使用PyArray_NewFromDescr
将缓冲区包装在一个ndarray
中,然后将OWNDATA
标志切换为 true。当释放ndarray
时,应调用ndarray
的PyDataMem_Handler
中的适当函数来释放缓冲区。但是PyDataMem_Handler
字段从未设置过,它将是NULL
。出于向后兼容性的原因,NumPy 将调用free()
来释放缓冲区。如果将NUMPY_WARN_IF_NO_MEM_POLICY
设置为1
,将发出警告。当前的默认设置是不发出警告,但在将来的 NumPy 版本可能会更改。
一个更好的技术是将 PyCapsule
用作基本对象:
/* define a PyCapsule_Destructor, using the correct deallocator for buff */
void free_wrap(void *capsule){
void * obj = PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule));
free(obj);
};
/* then inside the function that creates arr from buff */
...
arr = PyArray_NewFromDescr(... buf, ...);
if (arr == NULL) {
return NULL;
}
capsule = PyCapsule_New(buf, "my_wrapped_buffer",
(PyCapsule_Destructor)&free_wrap);
if (PyArray_SetBaseObject(arr, capsule) == -1) {
Py_DECREF(arr);
return NULL;
}
...
使用 np.lib.tracemalloc_domain
进行内存跟踪的示例
注意自 Python 3.6(或更新版本)以来,内置的 tracemalloc
模块可以用于跟踪 NumPy 内部的分配。NumPy 将其 CPU 内存分配放入 np.lib.tracemalloc_domain
域中。有关更多信息,请参阅:https://docs.python.org/3/library/tracemalloc.html。
这是一个使用 np.lib.tracemalloc_domain
的示例:
"""
The goal of this example is to show how to trace memory
from an application that has NumPy and non-NumPy sections.
We only select the sections using NumPy related calls.
"""
import tracemalloc
import numpy as np
# Flag to determine if we select NumPy domain
use_np_domain = True
nx = 300
ny = 500
# Start to trace memory
tracemalloc.start()
# Section 1
# ---------
# NumPy related call
a = np.zeros((nx,ny))
# non-NumPy related call
b = [i**2 for i in range(nx*ny)]
snapshot1 = tracemalloc.take_snapshot()
# We filter the snapshot to only select NumPy related calls
np_domain = np.lib.tracemalloc_domain
dom_filter = tracemalloc.DomainFilter(inclusive=use_np_domain,
domain=np_domain)
snapshot1 = snapshot1.filter_traces([dom_filter])
top_stats1 = snapshot1.statistics('traceback')
print("================ SNAPSHOT 1 =================")
for stat in top_stats1:
print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
print(stat.traceback.format()[-1])
# Clear traces of memory blocks allocated by Python
# before moving to the next section.
tracemalloc.clear_traces()
# Section 2
#----------
# We are only using NumPy
c = np.sum(a*a)
snapshot2 = tracemalloc.take_snapshot()
top_stats2 = snapshot2.statistics('traceback')
print()
print("================ SNAPSHOT 2 =================")
for stat in top_stats2:
print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
print(stat.traceback.format()[-1])
tracemalloc.stop()
print()
print("============================================")
print("\nTracing Status : ", tracemalloc.is_tracing())
try:
print("\nTrying to Take Snapshot After Tracing is Stopped.")
snap = tracemalloc.take_snapshot()
except Exception as e:
print("Exception : ", e)
历史概览
从版本 1.7.0 开始,NumPy 公开了一组 PyDataMem_*
函数(PyDataMem_NEW
、PyDataMem_FREE
、PyDataMem_RENEW
),它们分别由 alloc、free、realloc 支持。在这个版本中,NumPy 还公开了 PyDataMem_EventHook 功能(现在已弃用),描述如下,它封装了 OS 级别的调用。
自那时起,Python 也提高了其内存管理能力,并从 3.4 版本开始提供了各种管理策略。这些例程被分为一组域,每个域都有一组用于内存管理的 PyMemAllocatorEx
结构的例程。Python 还添加了一个 tracemalloc
模块来跟踪各种例程的调用。这些跟踪钩子被添加到 NumPy PyDataMem_*
例程中。
NumPy 在其内部的 npy_alloc_cache
、npy_alloc_cache_zero
和 npy_free_cache
函数中增加了一小块分配的内存缓存。这些函数分别封装了alloc
、alloc-and-memset(0)
和free
,但是当调用npy_free_cache
时,它会将指针添加到一个短的可用块列表中并标记大小。这些块可以被后续对 npy_alloc*
的调用重新使用,避免内存抖动。
NumPy 中的可配置内存例程 (NEP 49)
用户可能希望用自己的内部数据内存例程覆盖内部数据内存例程。由于 NumPy 不使用 Python 域策略来管理数据内存,它提供了一组替代的 C-API 来更改内存例程。对于大块对象数据,Python 领域没有全局策略,因此这些策略不太适合 NumPy 的需要。希望更改 NumPy 数据内存管理例程的用户可以使用PyDataMem_SetHandler
,它使用一个PyDataMem_Handler
结构来保存用于管理数据内存的函数指针。调用仍然由内部例程包装以调用PyTraceMalloc_Track
,PyTraceMalloc_Untrack
,并将使用已弃用的PyDataMem_EventHookFunc
机制。由于函数可能在进程的生命周期中发生变化,因此每个ndarray
都携带了其实例化时使用的函数,并且这些函数将用于重新分配或释放实例的数据内存。
type PyDataMem_Handler
用于保存用于操作内存的函数指针的结构
typedef struct {
char name[127]; /* multiple of 64 to keep the struct aligned */
uint8_t version; /* currently 1 */
PyDataMemAllocator allocator;
} PyDataMem_Handler;
分配器结构所在位置
/* The declaration of free differs from PyMemAllocatorEx */
typedef struct {
void *ctx;
void* (*malloc) (void *ctx, size_t size);
void* (*calloc) (void *ctx, size_t nelem, size_t elsize);
void* (*realloc) (void *ctx, void *ptr, size_t new_size);
void (*free) (void *ctx, void *ptr, size_t size);
} PyDataMemAllocator;
*PyDataMem_SetHandler( *handler)
设置新的分配策略。如果输入值为NULL
,将重置策略为默认值。返回上一个策略,如果发生错误则返回NULL
。我们包装了用户提供的函数,以便它们仍然调用 Python 和 numpy 内存管理回调挂钩。
*PyDataMem_GetHandler()
返回将用于为下一个PyArrayObject
分配数据的当前策略。失败时,返回NULL
。
有关设置和使用 PyDataMem_Handler 的示例,请参见numpy/core/tests/test_mem_policy.py
中的测试
void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data);
此函数将在数据内存操作期间调用
*PyDataMem_SetEventHook( *newhook, void *user_data, void **old_data)
设置 numpy 数组数据的分配事件挂钩。
返回指向上一个挂钩的指针,或NULL
。如果old_data
非NULL
,则将将前一个user_data
指针复制到其中。
如果非NULL
,将在每个PyDataMem_NEW/FREE/RENEW
结束时调用挂钩:
result = PyDataMem_NEW(size) -> (*hook)(NULL, result, size, user_data)
PyDataMem_FREE(ptr) -> (*hook)(ptr, NULL, 0, user_data)
result = PyDataMem_RENEW(ptr, size) -> (*hook)(ptr, result, size, user_data)
调用挂钩时,由调用线程持有 GIL。如果挂钩执行可能导致新的分配事件(如创建/销毁 numpy 对象,或创建/销毁可能导致 gc 的 Python 对象)的操作,应将挂钩编写为可重入。
已弃用于 v1.23
如果没有设置策略,则在释放内存时会发生什么
一种罕见但有用的技术是在 NumPy 之外分配一个缓冲区,使用PyArray_NewFromDescr
将缓冲区包装在ndarray
中,然后将OWNDATA
标志切换为 true。当释放ndarray
时,应调用ndarray
的PyDataMem_Handler
中的适当函数来释放缓冲区。但PyDataMem_Handler
字段从未被设置过,它将为NULL
。为了向后兼容,NumPy 将调用free()
来释放缓冲区。如果将NUMPY_WARN_IF_NO_MEM_POLICY
设置为1
,则会发出警告。目前的默认设置是不发出警告,这在将来的 NumPy 版本中可能会改变。
更好的技术是使用PyCapsule
作为基本对象。
/* define a PyCapsule_Destructor, using the correct deallocator for buff */
void free_wrap(void *capsule){
void * obj = PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule));
free(obj);
};
/* then inside the function that creates arr from buff */
...
arr = PyArray_NewFromDescr(... buf, ...);
if (arr == NULL) {
return NULL;
}
capsule = PyCapsule_New(buf, "my_wrapped_buffer",
(PyCapsule_Destructor)&free_wrap);
if (PyArray_SetBaseObject(arr, capsule) == -1) {
Py_DECREF(arr);
return NULL;
}
...
使用np.lib.tracemalloc_domain
进行内存跟踪的示例。
请注意,自 Python 3.6(或更新版本)以来,内置的tracemalloc
模块可以用于跟踪 NumPy 内的分配。NumPy 将其 CPU 内存分配放入np.lib.tracemalloc_domain
域中。有关附加信息,请查看:https://docs.python.org/3/library/tracemalloc.html。
这是使用np.lib.tracemalloc_domain
的示例。
"""
The goal of this example is to show how to trace memory
from an application that has NumPy and non-NumPy sections.
We only select the sections using NumPy related calls.
"""
import tracemalloc
import numpy as np
# Flag to determine if we select NumPy domain
use_np_domain = True
nx = 300
ny = 500
# Start to trace memory
tracemalloc.start()
# Section 1
# ---------
# NumPy related call
a = np.zeros((nx,ny))
# non-NumPy related call
b = [i**2 for i in range(nx*ny)]
snapshot1 = tracemalloc.take_snapshot()
# We filter the snapshot to only select NumPy related calls
np_domain = np.lib.tracemalloc_domain
dom_filter = tracemalloc.DomainFilter(inclusive=use_np_domain,
domain=np_domain)
snapshot1 = snapshot1.filter_traces([dom_filter])
top_stats1 = snapshot1.statistics('traceback')
print("================ SNAPSHOT 1 =================")
for stat in top_stats1:
print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
print(stat.traceback.format()[-1])
# Clear traces of memory blocks allocated by Python
# before moving to the next section.
tracemalloc.clear_traces()
# Section 2
#----------
# We are only using NumPy
c = np.sum(a*a)
snapshot2 = tracemalloc.take_snapshot()
top_stats2 = snapshot2.statistics('traceback')
print()
print("================ SNAPSHOT 2 =================")
for stat in top_stats2:
print(f"{stat.count} memory blocks: {stat.size / 1024:.1f} KiB")
print(stat.traceback.format()[-1])
tracemalloc.stop()
print()
print("============================================")
print("\nTracing Status : ", tracemalloc.is_tracing())
try:
print("\nTrying to Take Snapshot After Tracing is Stopped.")
snap = tracemalloc.take_snapshot()
except Exception as e:
print("Exception : ", e)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
2021-06-24 如何评价「施一公请辞清华大学副校长,全职执掌西湖大学」?你如何看待西湖大学的发展前景?