NumPy用户指南 >使用NumPy C-API > 如何扩展NumPy
尽管ndarray对象旨在允许在Python中进行快速计算,但它也旨在具有通用性并满足各种各样的计算需求。因此,如果绝对速度是必不可少的,则无法替代针对您的应用程序和硬件的精心设计的编译循环。这是numpy包含f2py的原因之一,因此可以使用一种易于使用的机制将(简单)C / C ++和(任意)Fortran代码直接链接到Python。鼓励您使用和改进此机制。本部分的目的不是记录此工具,而是记录编写该工具所依赖的扩展模块的更基本的步骤。
将扩展模块编写,编译并安装到Python路径(sys.path)中的某个位置后,可以将该代码导入到Python中,就好像它是标准python文件一样。它将包含用C代码定义和编译的对象和方法。在Python中执行此操作的基本步骤已被详细记录,您可以在www.python.org上在线获取Python本身的文档中找到更多信息。
除了Python C-API之外,还有用于NumPy的完整且丰富的C-API,允许在C级上进行复杂的操作。但是,对于大多数应用程序,通常仅使用少数API调用。如果您需要做的就是提取指向内存的指针以及一些形状信息以传递给另一个计算例程,那么您将使用完全不同的调用,那么如果您尝试创建类似数组的新类型或添加新数据ndarrays的类型。本章介绍最常用的API调用和宏。
必须在C代码中定义一个函数,Python才能将其用作扩展模块。该函数必须称为init {name},其中{name}是Python中模块的名称。必须声明此函数,以便在例程外的代码中可见。除了添加所需的方法和常量外,此子例程还必须包含调用,例如import_array()
和/或import_ufunc()
取决于所需的C-API。一旦实际调用任何C-API子例程,忘记放置这些命令将显示为难看的分段错误(崩溃)。实际上,在一个文件中可能有多个init {name}函数,在这种情况下,该文件将定义多个模块。但是,有一些技巧可以使它正常工作,这里不介绍。
最小init{name}
方法如下:
PyMODINIT_FUNC
init{name}(void)
{
(void)Py_InitModule({name}, mymethods);
import_array();
}
Mymethod必须是PyMethodDef结构的数组(通常是静态声明的),其中包含方法名称,实际C函数,指示方法是否使用关键字参数的变量以及文档字符串。这些将在下一节中说明。如果要向模块添加常量,则存储从Py_InitModule返回的值,该Py_InitModule是模块对象。向模块添加项目的最通用方法是使用PyModule_GetDict(module)获取模块字典。使用模块字典,您可以将所需的内容手动添加到模块中。将对象添加到模块的一种更简单的方法是使用三个额外的Python C-API调用之一,这些调用不需要单独提取模块字典。这些已记录在Python文档中,但为方便起见在此重复:
PyModule_AddStringConstant
(PyObject * 模块,char * 名称,char * 值)¶所有这三个函数都需要模块对象(Py_InitModule的返回值)。该名称是标签模块中的值的字符串。根据所调用的函数,value参数可以是通用对象(PyModule_AddObject
窃取对其的引用),整数常量或字符串常量。
传递给Py_InitModule函数的第二个参数是一种结构,可轻松在模块中定义函数。在上面给出的示例中,mymethods结构将在文件的较早位置(通常在init {name}子例程之前)定义为:
static PyMethodDef mymethods[] = {
{ nokeywordfunc,nokeyword_cfunc,
METH_VARARGS,
Doc string},
{ keywordfunc, keyword_cfunc,
METH_VARARGS|METH_KEYWORDS,
Doc string},
{NULL, NULL, 0, NULL} /* Sentinel */
}
mymethods数组中的每个条目都是一个PyMethodDef
结构,其中包含1)Python名称,2)实现该函数的C函数,3)指示该函数是否接受关键字的标志以及4)该函数的文档字符串。通过向该表添加更多条目,可以为单个模块定义任意数量的功能。如图所示,最后一个条目必须全为NULL,以充当哨兵。Python查找该条目以了解模块的所有功能均已定义。
完成扩展模块必须做的最后一件事是实际编写执行所需功能的代码。有两种功能:不接受关键字参数的函数和可以接受的参数。
不接受关键字参数的函数应写为:
static PyObject*
nokeyword_cfunc (PyObject *dummy, PyObject *args)
{
/* convert Python arguments */
/* do function */
/* return something */
}
在此上下文中不使用dummy参数,可以安全地将其忽略。该ARGS参数包含所有的到的功能作为一个元组传递的参数。您可以在此时进行任何操作,但是通常,管理输入参数的最简单方法是调用PyArg_ParseTuple
(args,format_string,addresss_to_C_variables…)或PyArg_UnpackTuple
(元组,“名称”,最小,最大...)。Python C-API参考手册中的5.5节(解析参数和构建值)中包含有关如何使用第一个函数的详细说明。您应该特别注意“ O&”格式,该格式使用转换器函数在Python对象和C对象之间移动。所有其他格式功能都可以(通常)视为该通用规则的特殊情况。在NumPy C-API中定义了一些可能有用的转换器函数。特别是,该PyArray_DescrConverter
功能对于支持任意数据类型规范非常有用。此函数将任何有效的数据类型Python对象转换为一个
对象。请记住传递应该填写的C变量的地址。PyArray_Descr *
关于如何在PyArg_ParseTuple
整个NumPy源代码中使用的示例很多。标准用法是这样的:
PyObject *input;
PyArray_Descr *dtype;
if (!PyArg_ParseTuple(args, "OO&", &input,
PyArray_DescrConverter,
&dtype)) return NULL;
重要的是要记住,你会得到一个很重要借来使用“O”格式字符串时参考的对象。但是,转换器功能通常需要某种形式的内存处理。在此示例中,如果转换成功,则dtype将保存对对象的新引用,而input将保存借用的引用。因此,如果此转换与另一个转换(例如,转换为整数)混合在一起,并且数据类型转换成功,但是整数转换失败,那么您需要在返回之前将引用计数释放到数据类型对象。一种典型的方法是
在调用之前将dtype设置为,然后
在dtype上使用PyArray_Descr *
NULL
PyArg_ParseTuple
Py_XDECREF
返回之前。
在处理完输入参数之后,将编写实际完成工作的代码(可能会根据需要调用其他函数)。C函数的最后一步是返回一些东西。如果遇到错误,NULL
则应返回(确保已实际设置了错误)。如果不返回任何内容,则递增
Py_None
并返回。如果应该返回单个对象,则将其返回(确保您首先拥有对该对象的引用)。如果应该返回多个对象,则需要返回一个元组。的Py_BuildValue
(format_string,c_variables…)函数使从C变量构建Python对象的元组变得容易。请特别注意格式字符串中“ N”和“ O”之间的区别,否则您很容易造成内存泄漏。“ O”格式字符串会增加其对应的C变量的引用计数,而“ N”格式字符串会窃取对相应C变量的引用。如果已经为对象创建了引用,并且只想将该引用提供给元组,则应使用“ N”。如果您仅借用了一个对象的引用,并且需要创建一个对象来提供元组,则应使用“ O”。PyObject *
PyObject *
这些功能与没有关键字参数的功能非常相似。唯一的区别是函数签名是:
static PyObject*
keyword_cfunc (PyObject *dummy, PyObject *args, PyObject *kwds)
{
...
}
kwds参数包含一个Python字典,其字典的键是关键字参数的名称,其值是相应的关键字参数值。可以处理此词典,但您认为合适。但是,最简单的处理方法是将
PyArg_ParseTuple
(args,format_string PyArg_ParseTupleAndKeywords
,addresss ...)函数替换为对(args,kwds,format_string,char * kwlist [],addresss ...)的调用。此函数的kwlist参数是一个NULL
以字符串结尾的字符串,提供了预期的关键字参数。format_string中的每个条目应该有一个字符串。如果传入无效的关键字参数,则使用此函数将引发TypeError。
有关此功能的更多帮助,请参见Python文档中“扩展和嵌入”教程的第1.8节(扩展功能的关键字参数)。
编写扩展模块时最大的困难是引用计数。这是f2py,weave,Cython,ctypes等流行的重要原因。如果您错误地处理了引用计数,则可能会遇到从内存泄漏到分段错误的问题。我知道正确处理参考计数的唯一策略是血液,汗水和眼泪。首先,您必须让每个Python变量都有一个引用计数。然后,您可以准确地了解每个函数对对象的引用计数的作用,以便在需要它们时可以正确使用DECREF和INCREF。引用计数可以真正测试您对编程技巧的耐心和勤奋程度。尽管描述严峻,大多数引用计数情况非常简单,最常见的困难是由于某些错误,在从例程中提前退出之前不对对象使用DECREF。其次,常见的错误是在传递给要窃取引用的函数或宏的对象上不拥有引用(例如 PyTuple_SET_ITEM
,以及大多数带有PyArray_Descr
对象的函数)。
通常,创建变量时它会得到一个新的引用,或者它是某个函数的返回值(但是,有一些突出的例外情况,例如从元组或字典中删除项目)。当您拥有引用时,您有责任确保Py_DECREF
在不再需要该变量(并且没有其他函数“窃取”其引用)时调用(var)。另外,如果将Python对象传递给将“窃取”引用的函数,则需要确保自己拥有该Py_INCREF
引用(或用于获取自己的引用)。您还将遇到借用参考的概念。借用引用的函数不会更改对象的引用计数,并且不会期望“保留”该引用。只是临时使用该对象。使用时PyArg_ParseTuple
否则,
PyArg_UnpackTuple
您会收到对元组中的对象的借用引用,并且不应在函数中更改它们的引用计数。通过实践,您可以学习正确地进行引用计数,但是起初可能会感到沮丧。
Py_BuildValue
函数是引用计数错误的一种常见来源。请注意“ N”格式字符和“ O”格式字符之间的区别。如果您在子例程(例如输出数组)中创建了一个新对象,并且将其以返回值的元组传递回去,那么您很可能应该在其中使用'N'格式字符Py_BuildValue
。“ O”字符会将引用计数增加一。这将为调用者提供一个全新数组的两个引用计数。当删除该变量并将引用计数减一时,仍然会有额外的引用计数,并且该数组将永远不会被释放。您将有一个引用计数导致的内存泄漏。使用'N'字符可以避免这种情况,因为它将返回给调用方一个具有单个引用计数的对象(在元组内部)。
NumPy的大多数扩展模块将需要访问ndarray对象(或其子类之一)的内存。执行此操作的最简单方法不需要您对NumPy的内部知识了解太多。方法是
确保您处理的是格式正确且尺寸正确的行为良好的数组(以机器字节顺序和单段对齐)。
通过使用
PyArray_FromAny
或在其上构建的宏将其从某些Python对象转换而来 。通过使用您想要的形状和类型的新ndarray
PyArray_NewFromDescr
或基于它的更简单的宏或函数。
获取数组的形状和指向其实际数据的指针。
将数据和形状信息传递到实际执行计算的子例程或代码的其他部分。
如果您正在编写算法,则建议您使用数组中包含的步幅信息来访问数组的元素(PyArray_GetPtr
宏使这一过程变得很轻松)。然后,您可以放宽要求,以免强制执行单段阵列以及可能导致的数据复制。
以下小节介绍了每个子主题。
从任何可以转换为数组的Python对象获取数组的主要例程是PyArray_FromAny
。该函数非常灵活,具有许多输入参数。几个宏使使用基本功能更加容易。PyArray_FROM_OTF
对于最常见的用途,可以说是最有用的宏。它允许您在指定一组特定要求(例如连续,对齐和可写)的同时,将任意Python对象转换为特定内置数据类型(例如 float)的数组。语法是
从任何Python对象obj返回一个ndarray,该对象可以转换为数组。返回数组中的维数由对象确定。返回的数组的所需数据类型以typenum提供,该类型应为枚举类型之一。返回数组的要求可以是标准数组标志的任意组合。这些论点中的每一个将在下面更详细地说明。成功时,您将收到对该阵列的新引用。失败时,
NULL
将返回并设置异常。对象
该对象可以是可转换为ndarray的任何Python对象。如果对象已经是满足要求的ndarray(的子类),则返回新的引用。否则,将构造一个新的数组。 除非使用数组接口,否则obj的内容将被复制到新数组中,从而不必复制数据。可以转换为数组的对象包括:1)任何嵌套序列对象,2)任何暴露数组接口的对象,3)任何带有
__array__
方法的对象(应返回ndarray),以及4)任何标量对象(成为a)零维数组)。否则将通过ndarray的子类满足要求。如果要确保基类ndarray,请使用NPY_ARRAY_ENSUREARRAY
在需求标记中。仅在必要时进行复制。如果要保证副本,请传递NPY_ARRAY_ENSURECOPY
到需求标记。Typenum
枚举类型之一,或者
NPY_NOTYPE
是否应从对象本身确定数据类型。可以使用基于C的名称:或者,可以使用平台上支持的位宽名称。例如:
仅在不损失精度的情况下,对象才会转换为所需的类型。否则
NULL
将返回并引发错误。使用NPY_ARRAY_FORCECAST
在要求标志覆盖此行为。要求
ndarray的内存模型允许在每个维度上任意步长前进到数组的下一个元素。但是,通常,您需要与需要C连续或Fortran连续内存布局的代码交互。此外,ndarray可能未对齐(元素的地址不是该元素的大小的整数倍),如果您尝试取消对指针的引用,可能会导致程序崩溃(或至少运行得更慢)。数组数据。通过将Python对象转换成对您的特定用法更“规范”的数组,可以解决这两个问题。
需求标志允许指定可接受的数组类型。如果传入的对象不满足此要求,则进行复制,以使返回的对象满足要求。这些ndarray可以使用非常通用的内存指针。该标志允许指定返回数组对象的所需属性。在详细的API章节中说明了所有标志。最常用的标志是
NPY_ARRAY_IN_ARRAY
,NPY_OUT_ARRAY
和NPY_ARRAY_INOUT_ARRAY
:该标志对于必须为C连续顺序并对齐的数组很有用。这些数组通常是某些算法的输入数组。
此标志对于指定C连续顺序,已对齐并且也可以写入的数组很有用。这样的数组通常作为输出返回(尽管通常这样的输出数组是从头开始创建的)。
该标志对于指定将用于输入和输出的数组很有用。
PyArray_ResolveWritebackIfCopy
必须Py_DECREF
在接口例程末尾之前调用,以将临时数据写回到传入的原始数组中。使用NPY_ARRAY_WRITEBACKIFCOPY
或NPY_ARRAY_UPDATEIFCOPY
标志要求输入对象已经是一个数组(因为其他对象无法以这种方式自动更新) 。如果发生错误,请PyArray_DiscardWritebackIfCopy
在设置了这些标志的数组上使用(obj)。这将使基础基本数组设置为可写,而不会导致将内容复制回原始数组。可以根据其他要求进行“或”运算的其他有用标志包括:
强制转换为所需的类型,即使不丢失信息也无法完成。
确保结果数组是原始数组的副本。
确保结果对象是实际的ndarray而不是子类。
注意
数组是否被字节交换取决于数组的数据类型。始终由来请求本机字节顺序数组PyArray_FROM_OTF
,因此NPY_ARRAY_NOTSWAPPED
在requirements参数中不需要标记。也没有办法从该例程中获取字节交换数组。
通常,必须从扩展模块代码中创建新数组。也许需要一个输出数组,而您不希望调用者必须提供它。也许只需要一个临时数组即可进行中间计算。无论需要什么,都有简单的方法来获取需要的任何数据类型的ndarray对象。最通用的功能是PyArray_NewFromDescr
。所有数组创建功能都通过此大量重复使用的代码进行。由于其灵活性,使用起来可能有些混乱。结果,存在更易于使用的更简单形式。这些形式是PyArray_SimpleNew
功能家族的一部分
,它们通过提供常见用例的默认值来简化界面。
如果obj是一个ndarray(),则ndarray 的数据区域由void *指针(obj)或char *指针(obj)指向。请记住,(通常)此数据区域可能未根据数据类型对齐,可能表示字节交换数据,并且/或者可能不可写。如果数据区域是对齐的并且是本机字节顺序,则如何获取数组的特定元素仅由npy_intp变量(obj)的数组确定。特别是,此整数c数组显示必须将多少个字节添加到当前元素指针才能到达每个维度中的下一个元素。对于小于4维的数组,有PyArrayObject *
PyArray_DATA
PyArray_BYTES
PyArray_STRIDES
PyArray_GETPTR{k}
(obj,…)宏,其中{k}是整数1、2、3或4,这使得使用数组的步幅更容易。争论...。表示数组中的{k}个非负整数索引。例如,假设E
是3维ndarray。指向元素的(void *)指针E[i,j,k]
作为PyArray_GETPTR3
(E,i,j,k)获得。
如前所述,C样式的连续数组和Fortran样式的连续数组具有特定的跨步模式。两个数组标志(NPY_ARRAY_C_CONTIGUOUS
和NPY_ARRAY_F_CONTIGUOUS
)指示特定数组的跨步模式是否与C风格的连续或Fortran风格的连续匹配或两者都不匹配。跨步模式是否与标准C或Fortran匹配,可以使用PyArray_IS_C_CONTIGUOUS
(obj)和
PyArray_ISFORTRAN
(obj)。大多数第三方库都希望使用连续数组。但是,通常不难支持通用跨步。我鼓励您尽可能在您自己的代码中使用跨步信息,并保留用于包装第三方代码的单段需求。使用ndarray提供的跨步信息,而不是要求连续跨步,可以减少原本必须进行的复制。
下面的示例演示如何编写一个包装程序,该包装程序接受两个输入参数(将被转换为数组)和一个输出参数(必须为数组)。该函数返回None并更新输出数组。请注意,对于NumPy v1.14及更高版本,WRITEBACKIFCOPY语义的更新用法
static PyObject *
example_wrapper(PyObject *dummy, PyObject *args)
{
PyObject *arg1=NULL, *arg2=NULL, *out=NULL;
PyObject *arr1=NULL, *arr2=NULL, *oarr=NULL;
if (!PyArg_ParseTuple(args, "OOO!", &arg1, &arg2,
&PyArray_Type, &out)) return NULL;
arr1 = PyArray_FROM_OTF(arg1, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr1 == NULL) return NULL;
arr2 = PyArray_FROM_OTF(arg2, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr2 == NULL) goto fail;
#if NPY_API_VERSION >= 0x0000000c
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY2);
#else
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY);
#endif
if (oarr == NULL) goto fail;
/* code that makes use of arguments */
/* You will probably need at least
nd = PyArray_NDIM(<..>) -- number of dimensions
dims = PyArray_DIMS(<..>) -- npy_intp array of length nd
showing length in each dim.
dptr = (double *)PyArray_DATA(<..>) -- pointer to data.
If an error occurs goto fail.
*/
Py_DECREF(arr1);
Py_DECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_ResolveWritebackIfCopy(oarr);
#endif
Py_DECREF(oarr);
Py_INCREF(Py_None);
return Py_None;
fail:
Py_XDECREF(arr1);
Py_XDECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_DiscardWritebackIfCopy(oarr);
#endif
Py_XDECREF(oarr);
return NULL;
}