2.8.2 Python-C-Api

Python-C-API是标准Python解释器(即CPython)的基础。使用这个API可以在C和C++中写Python扩展模块。很明显,由于语言兼容性的优点,这些扩展模块可以调用任何用C或者C++写的函数。

当使用Python-C-API时,人们通常写许多样板化的代码,首先解析函数接收的参数,然后构建返回的类型。

优点

  • 不需要额外的库
  • 许多系层的控制
  • C++完全可用

不足

  • 可以需要一定的努力
  • 高代码成本
  • 必须编译
  • 高维护成本
  • 如果C-API改变无法向前兼容Python版本
  • 引用计数错误很容易出现,但是很难被跟踪。

注意 此处的Python-C-Api例子主要是用来演示。许多其他例子的确依赖它,因此,对于它如何工作有一个高层次的理解。在99%的使用场景下,使用替代技术会更好。

注意 因为引用计数很容易出现然而很难被跟踪,任何需要使用Python C-API的人都应该阅读官方Python文档关于对象、类型和引用计数的部分。此外,有一个名为cpychecker的工具可以发现引用计数的常见错误。

2.8.2.1 例子

下面的C扩展模块,让来自标准math库的cos函数在Python中可用:

In [ ]:

  1. /* 用Python-C-API封装来自math.h的cos函数的例子 */
  2. #include <Python.h>
  3. #include <math.h>
  4. /* wrapped cosine function */
  5. static PyObject* cos_func(PyObject* self, PyObject* args)
  6. {
  7. double value;
  8. double answer;
  9. /* parse the input, from python float to c double */
  10. if (!PyArg_ParseTuple(args, "d", &value))
  11. return NULL;
  12. /* if the above function returns -1, an appropriate Python exception will
  13. * have been set, and the function simply returns NULL
  14. */
  15. /* call cos from libm */
  16. answer = cos(value);
  17. /* construct the output from cos, from c double to python float */
  18. return Py_BuildValue("f", answer);
  19. }
  20. /* define functions in module */
  21. static PyMethodDef CosMethods[] =
  22. {
  23. {"cos_func", cos_func, METH_VARARGS, "evaluate the cosine"},
  24. {NULL, NULL, 0, NULL}
  25. };
  26. /* module initialization */
  27. PyMODINIT_FUNC
  28. initcos_module(void)
  29. {
  30. (void) Py_InitModule("cos_module", CosMethods);
  31. }

如你所见,有许多样板,既包括 «massage» 的参数和return类型以及模块初始化。尽管随着扩展的增长,这些东西中的一些是分期偿还,模板每个函数需要的模板还是一样的。

标准python构建系统distutils支持从setup.py编译C-扩展, 非常方便:

In [ ]:

  1. from distutils.core import setup, Extension
  2. # 定义扩展模块
  3. cos_module = Extension('cos_module', sources=['cos_module.c'])
  4. # 运行setup
  5. setup(ext_modules=[cos_module])

这可以被编译:

  1. $ cd advanced/interfacing_with_c/python_c_api
  2. $ ls
  3. cos_module.c setup.py
  4. $ python setup.py build_ext --inplace
  5. running build_ext
  6. building 'cos_module' extension
  7. creating build
  8. creating build/temp.linux-x86_64-2.7
  9. gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/include/python2.7 -c cos_module.c -o build/temp.linux-x86_64-2.7/cos_module.o
  10. gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/python_c_api/cos_module.so
  11. $ ls
  12. build/ cos_module.c cos_module.so setup.py
  • build_ext 是构建扩展模块
  • —inplace 将把编译后的扩展模块输出到当前目录

文件cos_module.so包含编译后的扩展,我们可以在IPython解释器中加载它:

In [ ]:

  1. In [1]: import cos_module
  2. In [2]: cos_module?
  3. Type: module
  4. String Form:<module 'cos_module' from 'cos_module.so'>
  5. File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/python_c_api/cos_module.so
  6. Docstring: <no docstring>
  7. In [3]: dir(cos_module)
  8. Out[3]: ['__doc__', '__file__', '__name__', '__package__', 'cos_func']
  9. In [4]: cos_module.cos_func(1.0)
  10. Out[4]: 0.5403023058681398
  11. In [5]: cos_module.cos_func(0.0)
  12. Out[5]: 1.0
  13. In [6]: cos_module.cos_func(3.14159265359)
  14. Out[7]: -1.0

现在我们看一下这有多强壮:

In [ ]:

  1. In [10]: cos_module.cos_func('foo')
  2.  ------------- ------------- ------------- ------------- -----------------------
  3. TypeError Traceback (most recent call last)
  4. <ipython-input-10-11bee483665d> in <module>()
  5. ----> 1 cos_module.cos_func('foo')
  6. TypeError: a float is required

2.8.2.2. Numpy 支持

Numpy模拟Python-C-API, 自身也实现了C-扩展, 产生了Numpy-C-API。这个API可以被用来创建和操作来自C的Numpy数组, 当写一个自定义的C-扩展。也可以看一下:参考:advanced_numpy

注意 如果你确实需要使用Numpy C-API参考关于ArraysIterators的文档。

下列的例子显示如何将Numpy数组作为参数传递给函数,以及如果使用(旧)Numpy-C-API在Numpy数组上迭代。它只是将一个数组作为参数应用到来自math.h的cosine函数,并且返回生成的新数组。

In [ ]:

  1. /* 使用Numpy-C-API封装来自math.h的cos函数 . */
  2. #include <Python.h>
  3. #include <numpy/arrayobject.h>
  4. #include <math.h>
  5. /* 封装cosine函数 */
  6. static PyObject* cos_func_np(PyObject* self, PyObject* args)
  7. {
  8. PyArrayObject *in_array;
  9. PyObject *out_array;
  10. NpyIter *in_iter;
  11. NpyIter *out_iter;
  12. NpyIter_IterNextFunc *in_iternext;
  13. NpyIter_IterNextFunc *out_iternext;
  14. /* parse single numpy array argument */
  15. if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array))
  16. return NULL;
  17. /* construct the output array, like the input array */
  18. out_array = PyArray_NewLikeArray(in_array, NPY_ANYORDER, NULL, 0);
  19. if (out_array == NULL)
  20. return NULL;
  21. /* create the iterators */
  22. in_iter = NpyIter_New(in_array, NPY_ITER_READONLY, NPY_KEEPORDER,
  23. NPY_NO_CASTING, NULL);
  24. if (in_iter == NULL)
  25. goto fail;
  26. out_iter = NpyIter_New((PyArrayObject *)out_array, NPY_ITER_READWRITE,
  27. NPY_KEEPORDER, NPY_NO_CASTING, NULL);
  28. if (out_iter == NULL) {
  29. NpyIter_Deallocate(in_iter);
  30. goto fail;
  31. }
  32. in_iternext = NpyIter_GetIterNext(in_iter, NULL);
  33. out_iternext = NpyIter_GetIterNext(out_iter, NULL);
  34. if (in_iternext == NULL || out_iternext == NULL) {
  35. NpyIter_Deallocate(in_iter);
  36. NpyIter_Deallocate(out_iter);
  37. goto fail;
  38. }
  39. double ** in_dataptr = (double **) NpyIter_GetDataPtrArray(in_iter);
  40. double ** out_dataptr = (double **) NpyIter_GetDataPtrArray(out_iter);
  41. /* iterate over the arrays */
  42. do {
  43. **out_dataptr = cos(**in_dataptr);
  44. } while(in_iternext(in_iter) && out_iternext(out_iter));
  45. /* clean up and return the result */
  46. NpyIter_Deallocate(in_iter);
  47. NpyIter_Deallocate(out_iter);
  48. Py_INCREF(out_array);
  49. return out_array;
  50. /* in case bad things happen */
  51. fail:
  52. Py_XDECREF(out_array);
  53. return NULL;
  54. }
  55. /* 在模块中定义函数 */
  56. static PyMethodDef CosMethods[] =
  57. {
  58. {"cos_func_np", cos_func_np, METH_VARARGS,
  59. "evaluate the cosine on a numpy array"},
  60. {NULL, NULL, 0, NULL}
  61. };
  62. /* 模块初始化 */
  63. PyMODINIT_FUNC
  64. initcos_module_np(void)
  65. {
  66. (void) Py_InitModule("cos_module_np", CosMethods);
  67. /* IMPORTANT: this must be called */
  68. import_array();
  69. }

要编译这个模块,我们可以再用distutils。但是我们需要通过使用func:numpy.get_include确保包含了Numpy头部:

In [ ]:

  1. from distutils.core import setup, Extension
  2. import numpy
  3. # define the extension module
  4. cos_module_np = Extension('cos_module_np', sources=['cos_module_np.c'],
  5. include_dirs=[numpy.get_include()])
  6. # run the setup
  7. setup(ext_modules=[cos_module_np])

要说服我们自己这个方式确实有效,我们来跑一下下面的测试脚本:

In [ ]:

  1. import cos_module_np
  2. import numpy as np
  3. import pylab
  4. x = np.arange(0, 2 * np.pi, 0.1)
  5. y = cos_module_np.cos_func_np(x)
  6. pylab.plot(x, y)
  7. pylab.show()

这会产生以下的图像:

2.8.2 Python-C-Api - 图1