9.md 13.2 KB
Newer Older
W
wizardforcel 已提交
1
# 九、NumPy C-API 简介
W
wizardforcel 已提交
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

NumPy 是一个通用库,旨在满足科学应用程序开发人员的大多数需求。 但是,随着应用程序的代码库和覆盖范围的增加,计算也随之增加,有时用户需要更具体的操作和优化的代码段。 我们已经展示了 NumPy 和 Python 如何具有诸如 f2py 和 Cython 之类的工具来满足这些需求。 这些工具可能是将函数重写为本地编译代码以提高速度的绝佳选择。 但是在某些情况下(利用 C 库,例如 **NAG** 编写一些分析),您可能想做一些更根本的事情,例如专门创建新的数据结构 为您自己的图书馆。 这将要求您有权访问 Python 解释器中的低级控件。 在本章中,我们将研究如何使用 Python 及其扩展名 NumPy C-API 提供的 C-API 进行此操作。 C-API 本身是一个非常广泛的主题,可能需要一本书才能完全涵盖它。 在这里,我们将提供简短的介绍和示例,以帮助您开始使用 NumPy C-API。

本章将涉及的主题是:

*   Python C-API 和 NumPy C-API
*   扩展模块的基本结构
*   一些特定于 NumP 的 C-API 函数的简介
*   使用 C-API 创建函数
*   创建一个可调用的模块
*   通过 Python 解释器和其他模块使用模块

# Python 和 NumPy C-API

我们使用的 Python 实现是 Python 解释器的基于 C 的实现。 NumPy 专用于此基于 C 的 Python 实现。 Python 的此实现带有 C-API,它是解释器的基础,并向其用户提供低级控制。 NumPy 通过提供丰富的 C-API 进一步增强了这一功能。

用 C / C ++编写函数可以为开发人员提供灵活性,以利用这些语言提供的一些高级库。 但是,就必须在解析输入周围编写太多样板代码以构造返回值而言,代价显而易见。 此外,开发人员在引用/解引用对象时必须格外小心,因为这最终可能会导致讨厌的错误和内存泄漏。 随着 C-API 的不断发展,还存在代码未来兼容性的问题。 因此,如果开发人员想要迁移到更高版本的 Python,则他们可能需要为这些基于 C-API 的扩展进行大量维护工作。 由于这些困难,大多数开发人员选择尝试其他优化技术。 (例如 Cython 或 F2PY),然后再探索这条路径。 但是,在某些情况下,您可能想重用 C / C ++中的其他现有库,这可能适合您的特定目的。 在这些情况下,最好为现有函数编写包装并公开 Python 项目。

接下来,我们将看一些示例代码,并在本章继续介绍时解释其关键功能和宏。 此处提供的代码与 Python 2.X 版本兼容,可能不适用于 Python3.X。 但是,转换过程应该相似。

### 提示

开发人员可以尝试使用称为 **cpychecker** 的工具来检查模块中的引用计数时的常见错误。 请访问 [http://gcc-python-plugin.readthedocs.org/en/latest/cpychecker.html](http://gcc-python-plugin.readthedocs.org/en/latest/cpychecker.html) 了解更多详细信息。

# 扩展模块的基本结构

用 C 编写的扩展模块将包含以下部分:

*   标头段,其中包含所有外部库和`Python.h`
*   初始化段,您可以在其中定义模块名称和 C 模块中的功能
*   方法结构数组,用于定义模块中的所有功能
*   一个实现部分,您在其中定义要公开的所有功能

## 标头段

标题片段是非常标准的,就像普通的 C 模块一样。 我们需要包括`Python.h`头文件,以使我们的 C 代码可以访问 C-API 的内部。 该文件位于`<path_to_python>/include`中。 我们将在示例代码中使用数组对象,因此我们也包含了`numpy/arrayobject.h`头文件。 我们不需要在此处指定头文件的完整路径,因为路径解析是在`setup.py`中处理的,我们将在后面进行介绍:

```
/* 
Header Segment 
*/ 

#include <Python.h> 
#include <math.h> 
#include <numpy/arrayobject.h> 
Initialization Segment 

```

## 初始化段

初始化段从以下内容开始:

1.  调用`PyMODINIT_FUNC`宏。 此宏在 Python 标头中定义,并且在开始定义模块之前总是会被调用。
2.  下一行定义了初始化函数,并在加载该函数时由 Python 解释器调用。 函数名称必须为`init<module_name>`格式,C 代码将要公开的模块和函数的名称。

该函数的主体包含对`Py_InitModule3`的调用,该调用定义模块的名称和模块中的功能。 该函数的一般结构如下:

```
(void)Py_InitModule3(name_of_module, method_array, Docstring) 

```

`import_array()`的最终调用是特定于 NumPy 的函数,如果您的函数正在使用 Numpy Array 对象,则需要此函数。 这样可以确保加载 C-API,以便如果您的 C ++代码使用 C-API,则 API 表可用。 未能调用此函数和使用其他 NumPy API 函数将很可能导致分段错误错误。 建议您阅读 NumPy 文档中的`import_array()``import_ufunc()`

```
/* 
Initialization module 
*/ 

PyMODINIT_FUNC 
initnumpy_api_demo(void) 
{ 
(void)Py_InitModule3("numpy_api_demo", Api_methods, 
         "A demo to show Python and Numpy C-API"); 
import_array(); 
} 

```

## 方法结构数组

在此部分中,您将定义模块将要公开给 Python 的方法数组。 我们在这里定义了两个函数以求其平方。 一种方法将普通的 Python double 值作为输入,第二种方法对 Numpy 数组进行操作。 `PyMethodDef`结构可以在 C 中定义如下:

```
Struct PyMethodDef { 
char *method_name; 
PyCFunction method_function; 
int method_flags; 
char *method_docstring; 
}; 

```

这是此结构的成员的描述:

*   `method_name`:函数的名称在此处。 这将是函数向 Python 解释器公开的名称。
*   `method_function`:此变量保存在 Python 解释器中调用`method_name`时实际调用的 C 函数的名称。
*   `method_flags`:这告诉解释器我们的函数正在使用三个签名中的哪个。 该标志的值通常为`METH_VARARGS`。 如果要允许关键字参数进入函数,可以将该标志与`METH_KEYWORDS`组合。 它也可以具有`METH_NOARGS`的值,这表明您不想接受任何参数。
*   `method_docstring`:这是函数的文档字符串。

该结构需要以一个由 NULL 和 0 组成的标记终止,如以下示例所示:

```

/* 
Method array structure definition 
*/ 
static PyMethodDefApi_methods[] = 
{ 
{"py_square_func", square_func, METH_VARARGS, "evaluate the squares"}, 
{"np_square", square_nparray_func, METH_VARARGS,  "evaluates the square in numpy array"}, 
{NULL, NULL, 0, NULL} 
}; 

```

## 实施部分

实现部分是最直接的部分。 这就是方法的 C 定义所要去的地方。 在这里,我们将研究两个函数来平方它们的输入值。 这些函数的复杂度保持在较低水平,以便您专注于方法的结构。

# 使用 Python C-API 创建数组平方函数

Python 函数将对自身的引用作为第一个参数,然后是赋予该函数的真实参数。 `PyArg_ParseTuple`函数用于将 Python 函数中的值解析为 C 函数中的局部变量。 在此函数中,我们将值强制转换为双精度,因此我们将`d`用作第二个参数。 您可以在 [https://docs.python.org/2/c-api/arg.html](https://docs.python.org/2/c-api/arg.html) 上查看此函数接受的字符串的完整列表。

使用`Py_Buildvalue`返回计算的最终结果,它使用类似类型的格式字符串从您的答案中创建 Python 值。 我们在这里使用`f`表示浮点数,以证明对 double 和 float 的处理方式类似:

```
/* 
Implementation of the actual C funtions 
*/ 

static PyObject* square_func(PyObject* self, PyObject* args) 
{ 
double value; 
double answer; 

/*  parse the input, from python float to c double */ 
if (!PyArg_ParseTuple(args, "d", &value)) 
return NULL; 
/* if the above function returns -1, an appropriate Python exception will 
* have been set, and the function simply returns NULL 
*/ 

answer = value*value; 

return Py_BuildValue("f", answer); 
} 

```

# 使用 NumPy C-API 创建数组平方函数

在本节中,我们将创建一个函数以对 NumPy 数组的所有值求平方。 这里的目的是演示如何在 C 语言中获取 NumPy 数组,然后对其进行迭代。 在现实世界中,可以使用地图或通过矢量化平方函数以更简单的方式完成此操作。 我们正在使用与`O!`格式字符串相同的`PyArg_ParseTuple`函数。 该格式字符串具有`(object) [typeobject, PyObject *]`签名,并以 Python 类型对象作为第一个参数。 用户应阅读官方 API 文档,以查看允许使用其他格式的字符串以及哪种字符串适合他们的需求:

### 注意

如果传递的值的类型不同,则引发`TypeError`

以下代码段说明了如何使用`PyArg_ParseTuple`解析参数。

```
// Implementation of square of numpy array 

static PyObject* square_nparray_func(PyObject* self, PyObject* args) 
{ 

// variable declarations 
PyArrayObject *in_array; 
PyObject      *out_array; 
NpyIter *in_iter; 
NpyIter *out_iter; 
NpyIter_IterNextFunc *in_iternext; 
NpyIter_IterNextFunc *out_iternext; 

// Parse the argument tuple by specifying type "object" and putting the reference in in_array 
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array)) 
return NULL; 
...... 
...... 

```

下一步是创建一个数组以存储其输出值和迭代器,以便在 Numpy Arrays 上进行迭代。 请注意,创建对象时,每个步骤都有一个`{handle failure}`代码。 这是为了确保如果发生任何错误,我们可以通过调试来确定错误代码的位置:

```
//Construct the output from the new constructed input array 
out_array = PyArray_NewLikeArray(in_array, NPY_ANYORDER, NULL, 0); 
// Test it and if the input is nothing then just return nothing. 
{handle failure} 

//  Create the iterators 
in_iter = NpyIter_New(in_array, NPY_ITER_READONLY, NPY_KEEPORDER, 
NPY_NO_CASTING, NULL); 

// {handle failure} 

out_iter = NpyIter_New((PyArrayObject *)out_array, NPY_ITER_READWRITE, 
NPY_KEEPORDER, NPY_NO_CASTING, NULL); 
{handle failure} 

in_iternext = NpyIter_GetIterNext(in_iter, NULL); 
out_iternext = NpyIter_GetIterNext(out_iter, NULL); 
{handle failure} 

double ** in_dataptr = (double **) NpyIter_GetDataPtrArray(in_iter); 
double ** out_dataptr = (double **) NpyIter_GetDataPtrArray(out_iter); 

A simple handle failure module is like 
// {Start handling failure} 
if (in_iter == NULL) 
// remove the ref and return null 
Py_XDECREF(out_array); 
return NULL; 
// {End handling failure} 

```

看了前面的样板代码之后,我们终于来到了发生所有实际动作的部分。 那些熟悉 C ++的人会发现迭代方法与向量迭代相似。 我们之前定义的`in_iternext`函数在这里派上用场,用于迭代 Numpy 数组。 在 while 循环之后,我们确保在两个迭代器上都调用了`NpyIter_Deallocate`,在输出数组上调用了`Py_INCREF`; 未能调用这些函数是导致内存泄漏的最常见错误类型。 内存泄漏问题通常非常微妙,通常在具有长时间运行的代码(例如服务或守护程序)时才会出现。 要抓住这些问题,不幸的是,没有比使用调试器更深入的方法容易的方法了。 有时,只需要编写几个`printf`语句即可输出总内存使用情况:

```
/*  iterate over the arrays */ 
do { 
**out_dataptr =pow(**in_dataptr,2); 
} while(in_iternext(in_iter) && out_iternext(out_iter)); 

/*  clean up and return the result */ 
NpyIter_Deallocate(in_iter); 
NpyIter_Deallocate(out_iter); 
Py_INCREF(out_array); 
return out_array; 

```

# 构建和安装扩展模块

成功编写函数后,下一步是构建模块并在我们的 Python 模块中使用它。 `setup.py`文件看起来像以下代码片段:

```
from distutils.core import setup, Extension 
import numpy 
# define the extension module 
demo_module = Extension('numpy_api_demo', sources=['numpy_api.c'], 
include_dirs=[numpy.get_include()]) 

# run the setup 
setup(ext_modules=[demo_module]) 

```

由于我们使用特定于 NumPy 的标头,因此我们需要在`include_dirs`变量中具有`numpy.get_include`函数。 要运行此安装文件,我们将使用一个熟悉的命令:

```
python setup.py build_ext -inplace 

```

前面的命令将在目录中创建一个`numpy_api_demo.pyd`文件,供我们在 Python 解释器中使用。

为了测试我们的模块,我们将打开一个 Python 解释器测试,并尝试像我们对用 Python 编写的模块所做的一样,从该模块调用这些函数:

```
>>>import numpy_api_demo as npd 
>>> import numpy as np 
>>>npd.py_square_func(4) 
>>> 16.0 
>>> x = np.arange(0,10,1) 
>>> y = npd.np_square(x) 

```

# 摘要

在本章中,我们向您介绍了另一种使用 Python 和 NumPy 提供的 C-API 优化或集成 C / C ++代码的方法。 我们解释了该代码的基本结构以及其他示例代码,开发人员必须编写这些代码才能创建扩展模块。 之后,我们创建了两个函数,这些函数计算出一个数字的平方,并将该平方函数从`math.h`库映射到一个 Numpy 数组。 这里的目的是使您熟悉如何利用 C / C ++编写的数字库,以最少的代码重写来创建自己的模块。 编写 C 代码的范围比这里描述的要广泛得多。 但是,我们希望本章使您有信心在需要时利用 C-API。