2.8. Interfacing with C

作者Valentin Haenel

This chapter contains an introduction to the many different routes for making your native code (primarily C/C++) available from Python, a process commonly referred to wrapping. 本章的目标是为您提供什么技术存在的风格,它们各自的优点和缺点,以便您可以根据您的具体需求选择合适的技术。在任何情况下,一旦你开始包装,你几乎肯定会想要查阅您选择的技术的相应文档。

2.8.1. 简介

本章包括以下技术:

这四种技术可能是最知名的,其中Cython可能是最先进的,你应该考虑使用第一。其他的也很重要,如果你想从不同的角度来理解包装问题。话虽如此,有其他替代品,但是了解了上述的基础知识,你将能够评估你的选择的技术,看看它是否适合你的需要。

在评估技术时,以下标准可能有用:

  • 是否需要额外的库?
  • 代码是否自动生成?
  • 是否需要编译?
  • 是否有良好的支持与Numpy数组交互?
  • 它支持C ++吗?

在你出发之前,你应该考虑你的用例。当与本地代码连接时,通常有两个用例:

  • C / C ++中需要利用的现有代码,因为它已经存在,或者因为它更快。
  • Python代码太慢,将内部循环推送到本机代码

通过包装math.h中的cos函数演示每种技术。虽然这是一个很简单的例子,它应该能够很好地演示包装解决方案的基础。由于每种技术还包括某种形式的Numpy支持,这也使用其中在某种数组上计算余弦的示例来演示。

最后但同样重要的是,两个小警告:

  • 所有这些技术可能会崩溃(分段错误)Python解释器,这是(通常)由于C代码中的错误。
  • 所有示例都在Linux上完成,它们应该可能在其他操作系统上。
  • 对于大多数示例,您将需要一个C编译器。

2.8.2. Python-C-Api

Python-C-API是标准Python解释器的主干(a.k.a CPython)。使用这个API,可以用C和C ++编写Python扩展模块。显然,由于语言兼容性,这些扩展模块可以调用用C或C ++编写的任何函数。

当使用Python-C-API时,通常编写很多样板代码,首先解析给函数的参数,然后再构造返回类型。

优点

  • 不需要额外的库
  • 很多低级控制
  • 完全可以从C ++使用

缺点

  • 可能需要大量的努力
  • 代码中有很多开销
  • 必须编译
  • 维护成本高
  • 由于C-API的改变,Python版本之间没有向前兼容性
  • 引用计数错误很容易创建,很难跟踪。

注意

Python-C-Api示例在这里主要是为了教导的原因。许多其他技术实际上依赖于这一点,因此有一个高级别的理解它是如何工作是很好。在99%的使用情况下,你会更好,使用替代技术。

注意

因为引用计数bug很容易创建并难以跟踪,任何真正需要使用Python C-API的人都应该阅读官方python文档中有关对象,类型和引用计数的部分此外,有一个名为cpychecker的工具,它可以帮助发现引用计数的常见错误。

2.8.2.1. Example

以下C-extension模块,使得标准数学库中的cos函数可用于Python:

/*  Example of wrapping cos function from math.h with the Python-C-API. */
#include <Python.h>
#include <math.h>
/* wrapped cosine function */
static PyObject* cos_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
*/
/* call cos from libm */
answer = cos(value);
/* construct the output from cos, from c double to python float */
return Py_BuildValue("f", answer);
}
/* define functions in module */
static PyMethodDef CosMethods[] =
{
{"cos_func", cos_func, METH_VARARGS, "evaluate the cosine"},
{NULL, NULL, 0, NULL}
};
/* module initialization */
PyMODINIT_FUNC
initcos_module(void)
{
(void) Py_InitModule("cos_module", CosMethods);
}

正如你可以看到的,有很多样板,无论是«按摩»参数和返回类型到位和模块初始化。虽然其中一些是摊销,随着扩展增长,每个功能所需的样板仍然存在。

标准的python构建系统distutils支持从setup.py编译C扩展,这是相当方便的:

from distutils.core import setup, Extension
# define the extension module
cos_module = Extension('cos_module', sources=['cos_module.c'])
# run the setup
setup(ext_modules=[cos_module])

这可以编译:

$ cd advanced/interfacing_with_c/python_c_api
$ ls
cos_module.c setup.py
$ python setup.py build_ext --inplace
running build_ext
building 'cos_module' extension
creating build
creating build/temp.linux-x86_64-2.7
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
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
$ ls
build/ cos_module.c cos_module.so setup.py
  • build_ext是构建扩展模块
  • --inplace将把编译的扩展模块输出到当前目录

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

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

现在让我们看看这是多么鲁棒:

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

2.8.2.2. Numpy Support

与Python-C-API类似,Numpy本身作为C扩展实现,它带有Numpy-C-API在编写自定义C扩展时,此API可用于从C创建和操作Numpy数组。参见:Advanced NumPy

注意

如果您确实需要使用Numpy C-API,请参阅有关数组迭代器的文档。

以下示例显示如何将Numpy数组作为参数传递给函数,以及如何使用(old)Numpy-C-API对Numpy数组进行迭代。它只需要一个数组作为参数应用来自math.h的余弦函数,并返回一个结果的新数组。

/*  Example of wrapping the cos function from math.h using the Numpy-C-API. */
#include <Python.h>
#include <numpy/arrayobject.h>
#include <math.h>
/* wrapped cosine function */
static PyObject* cos_func_np(PyObject* self, PyObject* args)
{
PyArrayObject *in_array;
PyObject *out_array;
NpyIter *in_iter;
NpyIter *out_iter;
NpyIter_IterNextFunc *in_iternext;
NpyIter_IterNextFunc *out_iternext;
/* parse single numpy array argument */
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array))
return NULL;
/* construct the output array, like the input array */
out_array = PyArray_NewLikeArray(in_array, NPY_ANYORDER, NULL, 0);
if (out_array == NULL)
return NULL;
/* create the iterators */
in_iter = NpyIter_New(in_array, NPY_ITER_READONLY, NPY_KEEPORDER,
NPY_NO_CASTING, NULL);
if (in_iter == NULL)
goto fail;
out_iter = NpyIter_New((PyArrayObject *)out_array, NPY_ITER_READWRITE,
NPY_KEEPORDER, NPY_NO_CASTING, NULL);
if (out_iter == NULL) {
NpyIter_Deallocate(in_iter);
goto fail;
}
in_iternext = NpyIter_GetIterNext(in_iter, NULL);
out_iternext = NpyIter_GetIterNext(out_iter, NULL);
if (in_iternext == NULL || out_iternext == NULL) {
NpyIter_Deallocate(in_iter);
NpyIter_Deallocate(out_iter);
goto fail;
}
double ** in_dataptr = (double **) NpyIter_GetDataPtrArray(in_iter);
double ** out_dataptr = (double **) NpyIter_GetDataPtrArray(out_iter);
/* iterate over the arrays */
do {
**out_dataptr = cos(**in_dataptr);
} 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;
/* in case bad things happen */
fail:
Py_XDECREF(out_array);
return NULL;
}
/* define functions in module */
static PyMethodDef CosMethods[] =
{
{"cos_func_np", cos_func_np, METH_VARARGS,
"evaluate the cosine on a numpy array"},
{NULL, NULL, 0, NULL}
};
/* module initialization */
PyMODINIT_FUNC
initcos_module_np(void)
{
(void) Py_InitModule("cos_module_np", CosMethods);
/* IMPORTANT: this must be called */
import_array();
}

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

from distutils.core import setup, Extension
import numpy
# define the extension module
cos_module_np = Extension('cos_module_np', sources=['cos_module_np.c'],
include_dirs=[numpy.get_include()])
# run the setup
setup(ext_modules=[cos_module_np])

为了说服自己是否确实有效,我们运行以下测试脚本:

import cos_module_np
import numpy as np
import pylab
x = np.arange(0, 2 * np.pi, 0.1)
y = cos_module_np.cos_func_np(x)
pylab.plot(x, y)
pylab.show()

这应该导致下图:

../../_images/test_cos_module_np.png

2.8.3. Ctypes

Ctypes是Python的外部函数库它提供C兼容的数据类型,并允许在DLL或共享库中调用函数。它可以用于在纯Python中包装这些库。

优点

  • Python标准库的一部分
  • 不需要编译
  • 完全在Python中包装代码

缺点

  • Requires code to be wrapped to be available as a shared library (roughly speaking *.dll in Windows *.so in Linux and *.dylib in Mac OSX.)
  • 没有好的支持C ++

2.8.3.1. Example

如所公布的,包装代码是纯Python。

""" Example of wrapping cos function from math.h using ctypes. """
import ctypes
# find and load the library
# OSX or linux
from ctypes.util import find_library
libm = ctypes.cdll.LoadLibrary(find_library('m'))
# Windows
# from ctypes import windll
# libm = cdll.msvcrt
# set the argument type
libm.cos.argtypes = [ctypes.c_double]
# set the return type
libm.cos.restype = ctypes.c_double
def cos_func(arg):
''' Wrapper for cos from math.h '''
return libm.cos(arg)
  • 查找和加载库可能因您的操作系统而异,有关详细信息,请查看文档
  • 这可能有点欺骗,因为数学库已经在系统上以编译的形式存在。如果你要包装一个内部库,你必须先编译它,这可能需要一些额外的努力,也可能不需要。

我们现在可以使用这个,如前所述:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/ctypes/cos_module.py
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'cos_func',
'ctypes',
'find_library',
'libm']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[6]: -1.0

与前面的例子一样,这段代码有点健壮,虽然错误消息不太有用,因为它不告诉我们应该是什么类型。

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
ArgumentError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
/home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/ctypes/cos_module.py in cos_func(arg)
12 def cos_func(arg):
13 ''' Wrapper for cos from math.h '''
---> 14 return libm.cos(arg)
ArgumentError: argument 1: <type 'exceptions.TypeError'>: wrong type

2.8.3.2. Numpy Support

Numpy包含一些支持与ctypes的接口。特别是支持将Numpy数组的某些属性导出为ctypes数据类型,并且有从C数组转换为Numpy数组和返回的函数。

有关详细信息,请参阅Numpy Cookbook中的相应部分以及numpy.ndarray.ctypesnumpy.ctypeslib的API文档。

对于以下示例,让我们考虑一个库中的C函数,它接受输入和输出数组,计算输入数组的余弦值,并将结果存储在输出数组中。

该库包含以下头文件(虽然这不是这个例子的严格需要,我们列出它的完整性):

void cos_doubles(double * in_array, double * out_array, int size);

函数实现驻留在以下C源文件中:

#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

由于库是纯C,我们不能使用distutils来编译它,而必须使用makegcc的组合:

m.PHONY : clean
libcos_doubles.so : cos_doubles.o
gcc -shared -Wl,-soname,libcos_doubles.so -o libcos_doubles.so cos_doubles.o
cos_doubles.o : cos_doubles.c
gcc -c -fPIC cos_doubles.c -o cos_doubles.o
clean :
-rm -vf libcos_doubles.so cos_doubles.o cos_doubles.pyc

然后我们可以将这个(在Linux上)编译到共享库libcos_doubles.so

$ ls
cos_doubles.c cos_doubles.h cos_doubles.py makefile test_cos_doubles.py
$ make
gcc -c -fPIC cos_doubles.c -o cos_doubles.o
gcc -shared -Wl,-soname,libcos_doubles.so -o libcos_doubles.so cos_doubles.o
$ ls
cos_doubles.c cos_doubles.o libcos_doubles.so* test_cos_doubles.py
cos_doubles.h cos_doubles.py makefile

现在我们可以继续通过ctypes包装这个库,直接支持(某些种类)Numpy数组:

""" Example of wrapping a C library function that accepts a C double array as
input using the numpy.ctypeslib. """
import numpy as np
import numpy.ctypeslib as npct
from ctypes import c_int
# input type for the cos_doubles function
# must be a double array, with single dimension that is contiguous
array_1d_double = npct.ndpointer(dtype=np.double, ndim=1, flags='CONTIGUOUS')
# load the library, using numpy mechanisms
libcd = npct.load_library("libcos_doubles", ".")
# setup the return types and argument types
libcd.cos_doubles.restype = None
libcd.cos_doubles.argtypes = [array_1d_double, array_1d_double, c_int]
def cos_doubles_func(in_array, out_array):
return libcd.cos_doubles(in_array, out_array, len(in_array))
  • 注意连续单维Numpy数组的固有限制,因为C函数需要这种缓冲区。
  • 还要注意,输出数组必须预先分配,例如使用numpy.zeros(),函数将写入缓冲区。
  • 尽管cos_doubles函数的原始签名为ARRAY, ARRAY, int final cos_doubles_func只需要两个Numpy数组作为参数。

和以前一样,我们说服自己它的工作:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles.png

2.8.4. SWIG

SWIG是简化的包装器接口生成器,是一种软件开发工具,用于连接使用C和C ++编写的程序与各种高级编程语言(包括Python)。SWIG的重要事情是,它可以为您自动生成包装代码。虽然这是开发时间的优势,但它也可能是一种负担。所生成的文件往往相当大,并且可能不是太可读的,并且作为包装过程的结果的多个间接级别可能有点难以理解。

注意

自动生成的C代码使用Python-C-Api。

优点

  • 可以自动包装整个库给定的头
  • 与C ++很好地工作

缺点

  • 自动生成巨大的文件
  • 如果出现问题,很难调试
  • 陡峭的学习曲线

2.8.4.1. Example

让我们假设我们的cos函数存在于c中的cos_module中,并且包含源文件cos_module.c

#include <math.h>
double cos_func(double arg){
return cos(arg);
}

和头文件cos_module.h

double cos_func(double arg);

我们的目标是将cos_func暴露给Python。要使用SWIG实现这一点,我们必须编写一个包含SWIG指令的接口文件

/*  Example of wrapping cos function from math.h using SWIG. */
%module cos_module
%{
/* the resulting C file should be built as a python extension */
#define SWIG_FILE_WITH_INIT
/* Includes the header in the wrapper code */
#include "cos_module.h"
%}
/* Parse the header file to generate wrappers */
%include "cos_module.h"

正如你可以看到,这里不需要太多的代码。对于这个简单的例子,足以简单地将头文件包含在接口文件中,以将函数公开给Python。但是,SWIG允许更细粒度地包含/排除头文件中的函数,请检查文档以了解详细信息。

生成编译的包装器是一个两阶段过程:

  1. 对接口文件运行swig可执行文件以生成文件cos_module_wrap.c,这是自动生成的Python C扩展的源文件,cos_module.py
  2. cos_module_wrap.c编译为_cos_module.so幸运的是,distutils知道如何处理SWIG接口文件,因此我们的setup.py很简单:
from distutils.core import setup, Extension
setup(ext_modules=[Extension("_cos_module",
sources=["cos_module.c", "cos_module.i"])])
$ cd advanced/interfacing_with_c/swig
$ ls
cos_module.c cos_module.h cos_module.i setup.py
$ python setup.py build_ext --inplace
running build_ext
building '_cos_module' extension
swigging cos_module.i to cos_module_wrap.c
swig -python -o cos_module_wrap.c cos_module.i
creating build
creating build/temp.linux-x86_64-2.7
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
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_wrap.c -o build/temp.linux-x86_64-2.7/cos_module_wrap.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o build/temp.linux-x86_64-2.7/cos_module_wrap.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig/_cos_module.so
$ ls
build/ cos_module.c cos_module.h cos_module.i cos_module.py _cos_module.so* cos_module_wrap.c setup.py

我们现在可以加载并执行cos_module,就像我们在前面的示例中所做的那样:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig/cos_module.py
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'_cos_module',
'_newclass',
'_object',
'_swig_getattr',
'_swig_property',
'_swig_repr',
'_swig_setattr',
'_swig_setattr_nondynamic',
'cos_func']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[6]: -1.0

我们再次测试鲁棒性,我们看到我们得到了一个更好的错误消息(尽管严格来说,在Python中没有double类型):

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
TypeError: in method 'cos_func', argument 1 of type 'double'

2.8.4.2. Numpy Support

Numpy使用numpy.i文件为SWIG提供支持。该接口文件定义了支持Numpy数组和C-Arrays之间转换的各种所谓的类型映射在下面的示例中,我们将快速了解这种类型的映射在实践中如何工作。

我们有与ctypes示例中相同的cos_doubles函数:

void cos_doubles(double * in_array, double * out_array, int size);
#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

使用以下SWIG接口文件将其打包为cos_doubles_func

/*  Example of wrapping a C function that takes a C double array as input using
* numpy typemaps for SWIG. */
%module cos_doubles
%{
/* the resulting C file should be built as a python extension */
#define SWIG_FILE_WITH_INIT
/* Includes the header in the wrapper code */
#include "cos_doubles.h"
%}
/* include the numpy typemaps */
%include "numpy.i"
/* need this for correct module initialization */
%init %{
import_array();
%}
/* typemaps for the two arrays, the second will be modified in-place */
%apply (double* IN_ARRAY1, int DIM1) {(double * in_array, int size_in)}
%apply (double* INPLACE_ARRAY1, int DIM1) {(double * out_array, int size_out)}
/* Wrapper for cos_doubles that massages the types */
%inline %{
/* takes as input two numpy arrays */
void cos_doubles_func(double * in_array, int size_in, double * out_array, int size_out) {
/* calls the original funcion, providing only the size of the first */
cos_doubles(in_array, out_array, size_in);
}
%}
  • 要使用Numpy类型映射,我们需要包括numpy.i文件。
  • 观察我们在Numpy-C-API示例中遇到的对import_array()的调用。
  • 由于类型映射只支持签名ARRAY, SIZE,我们需要将cos_doubles包装为cos_doubles_func它采用两个数组,包括大小作为输入。
  • 与简单的SWIG示例相反,我们不包括cos_doubles.h头,没有什么我们希望暴露给Python,因为我们通过cos_doubles_func

和以前一样,我们可以使用distutils来包装:

from distutils.core import setup, Extension
import numpy
setup(ext_modules=[Extension("_cos_doubles",
sources=["cos_doubles.c", "cos_doubles.i"],
include_dirs=[numpy.get_include()])])

如前所述,我们需要使用include_dirs来指定位置。

$ ls
cos_doubles.c cos_doubles.h cos_doubles.i numpy.i setup.py test_cos_doubles.py
$ python setup.py build_ext -i
running build_ext
building '_cos_doubles' extension
swigging cos_doubles.i to cos_doubles_wrap.c
swig -python -o cos_doubles_wrap.c cos_doubles.i
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles.c -o build/temp.linux-x86_64-2.7/cos_doubles.o
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles_wrap.c -o build/temp.linux-x86_64-2.7/cos_doubles_wrap.o
In file included from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1722,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:17,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/arrayobject.h:15,
from cos_doubles_wrap.c:2706:
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/npy_deprecated_api.h:11:2: warning: #warning "Using deprecated NumPy API, disable it by #defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION"
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_doubles.o build/temp.linux-x86_64-2.7/cos_doubles_wrap.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig_numpy/_cos_doubles.so
$ ls
build/ cos_doubles.h cos_doubles.py cos_doubles_wrap.c setup.py
cos_doubles.c cos_doubles.i _cos_doubles.so* numpy.i test_cos_doubles.py

和以前一样,我们说服自己它的工作:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles1.png

2.8.5. Cython

Cython是一种类似Python的语言,用于编写C语言扩展和用于此语言的高级编译器。Cython 语言是Python的超集,它带有额外的构造,允许您调用C函数,并用c类型注释变量和类属性。在这个意义上,我们也可以把它叫做Python类型

除了包装本地代码的基本用例之外,Cython还支持一个额外的用例,即交互式优化。基本上,一个开始与一个纯Python脚本,并增量添加Cython类型到瓶颈代码只优化那些真正重要的代码路径。

在这个意义上,它与SWIG非常相似,因为代码可以自动生成,但在某种意义上它也非常类似于ctypes,因为包装代码(几乎)可以用Python编写。

而其他解决方案自动生成代码可能很难调试(例如SWIG)Cython自带了一个扩展到GNU调试器,帮助调试Python,Cython和C代码。

注意

自动生成的C代码使用Python-C-Api。

优点

  • Python语言编写C扩展
  • 自动生成代码
  • 支持增量优化
  • 包括一个GNU调试器扩展
  • 支持C ++(从0.13版开始)

缺点

  • 必须编译
  • 需要一个额外的库(但只有在构建时,在这个问题可以通过运送生成的C文件克服)

2.8.5.1. Example

我们的cos_module的主要Cython代码包含在文件cos_module.pyx中:

""" Example of wrapping cos function from math.h using Cython. """
cdef extern from "math.h":
double cos(double arg)
def cos_func(arg):
return cos(arg)

请注意其他关键字,例如cdefextern此外,cos_func是纯Python。

同样,我们可以使用标准的distutils模块,但是这次我们需要一些来自Cython.Distutils的附件:

from distutils.core import setup, Extension
from Cython.Distutils import build_ext
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("cos_module", ["cos_module.pyx"])]
)

编译这个:

$ cd advanced/interfacing_with_c/cython
$ ls
cos_module.pyx setup.py
$ python setup.py build_ext --inplace
running build_ext
cythoning cos_module.pyx to cos_module.c
building 'cos_module' extension
creating build
creating build/temp.linux-x86_64-2.7
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
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/cython/cos_module.so
$ ls
build/ cos_module.c cos_module.pyx cos_module.so* setup.py

运行它:

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

而且,测试一点鲁棒性,我们可以看到,我们得到好的错误消息:

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
/home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython/cos_module.so in cos_module.cos_func (cos_module.c:506)()
TypeError: a float is required

此外,值得注意的是,Cython附带了C数学库的完整声明,这简化了上面的代码:

""" Simpler example of wrapping cos function from math.h using Cython. """
from libc.math cimport cos
def cos_func(arg):
return cos(arg)

在这种情况下,cimport语句用于导入cos函数。

2.8.5.2. Numpy Support

Cython通过numpy.pyx文件支持Numpy,这允许您将Numpy数组类型添加到Cython代码中。也就是说像指定变量i的类型为int,可以指定变量a的类型为numpy.ndarray与给定的dtype此外,还支持某些优化(如边界检查)。查看Cython文档中的相应部分。如果你想将Numpy数组作为C数组传递给你的Cython包装的C函数,那么在Cython wiki中有一节。

在下面的示例中,我们将演示如何使用Cython包装熟悉的cos_doubles函数。

void cos_doubles(double * in_array, double * out_array, int size);
#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

使用以下Cython代码将其封装为cos_doubles_func

""" Example of wrapping a C function that takes C double arrays as input using
the Numpy declarations from Cython """
# cimport the Cython declarations for numpy
cimport numpy as np
# if you want to use the Numpy-C-API from Cython
# (not strictly necessary for this example, but good practice)
np.import_array()
# cdefine the signature of our c function
cdef extern from "cos_doubles.h":
void cos_doubles (double * in_array, double * out_array, int size)
# create the wrapper code, with numpy type annotations
def cos_doubles_func(np.ndarray[double, ndim=1, mode="c"] in_array not None,
np.ndarray[double, ndim=1, mode="c"] out_array not None):
cos_doubles(<double*> np.PyArray_DATA(in_array),
<double*> np.PyArray_DATA(out_array),
in_array.shape[0])

并且可以使用distutils编译:

from distutils.core import setup, Extension
import numpy
from Cython.Distutils import build_ext
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("cos_doubles",
sources=["_cos_doubles.pyx", "cos_doubles.c"],
include_dirs=[numpy.get_include()])],
)
  • 与之前编译的Numpy示例一样,我们需要include_dirs选项。
$ ls
cos_doubles.c cos_doubles.h _cos_doubles.pyx setup.py test_cos_doubles.py
$ python setup.py build_ext -i
running build_ext
cythoning _cos_doubles.pyx to _cos_doubles.c
building 'cos_doubles' extension
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c _cos_doubles.c -o build/temp.linux-x86_64-2.7/_cos_doubles.o
In file included from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1722,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:17,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/arrayobject.h:15,
from _cos_doubles.c:253:
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/npy_deprecated_api.h:11:2: warning: #warning "Using deprecated NumPy API, disable it by #defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION"
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/__ufunc_api.h:236: warning: ‘_import_umath’ defined but not used
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles.c -o build/temp.linux-x86_64-2.7/cos_doubles.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/_cos_doubles.o build/temp.linux-x86_64-2.7/cos_doubles.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython_numpy/cos_doubles.so
$ ls
build/ _cos_doubles.c cos_doubles.c cos_doubles.h _cos_doubles.pyx cos_doubles.so* setup.py test_cos_doubles.py

和以前一样,我们说服自己它的工作:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles2.png

2.8.6. 摘要

在本节中,提出了与本地代码接口的四种不同技术。下表大致总结了技术的一些方面。

X CPython的一部分 编译 自动生成 支持Numpy
Python-C-API True True False True
Ctypes True False False True
Swig False True True True
Cython False True True True

在所有三种呈现的技术中,Cython是最现代和最先进的。特别是,通过向Python代码添加类型来递增地优化代码的能力是独一无二的。

2.8.7. 进一步阅读和参考

2.8.8. 练习

由于这是一个全新的部分,练习被认为更多的指针,看看下一步,所以选择你觉得更有趣的。如果你有好的想法练习,请让我们知道!

  1. 下载每个示例的源代码,并在您的机器上编译和运行它们。
  2. 对每个示例进行微小的更改,并说服自己,这是工作。(例如对于sin改变cos
  3. 大多数例子,特别是涉及Numpy的例子可能仍然是脆弱的,并对输入错误作出严重反应。寻找方法来崩溃的例子,找出问题是什么,并设计一个潜在的解决方案。这里有一些想法:
    1. 数字溢出。
    2. 具有不同长度的输入和输出数组。
    3. 多维数组。
    4. 空数组
    5. double类型的数组
  4. 使用%timeit IPython魔法来衡量各种解决方案的执行时间

2.8.8.1. Python-C-API

  1. 修改Numpy示例,以使该函数接受两个输入参数,其中第二个是预分配的输出数组,使其类似于其他Numpy示例。
  2. 尝试修复此示例以使用新的Numpy迭代器协议如果你设法获得一个工作的解决方案,请在github上提交一个pull-request。
  3. 您可能已经注意到,Numpy-C-API示例是不包括cos_doubles但是将cos函数直接应用于Numpy的元素的唯一Numpy示例数组。这是否比其他技术有任何优势。
  4. 你可以只使用Numpy-C-API包装cos_doubles您可能需要确保数组具有正确的类型,在内存中是一维和连续的。

2.8.8.2. Ctypes

  1. 修改Numpy示例,使cos_doubles_func处理您的预分配,从而使它更像Numpy-C-API示例。

2.8.8.3. SWIG

  1. 看看SWIG自动生成的代码,你理解了多少?
  2. 修改Numpy示例,使cos_doubles_func处理您的预分配,从而使它更像Numpy-C-API示例。
  3. 修改cos_doubles C函数,以便返回已分配的数组。你可以使用SWIG类型打包吗?如果不是,为什么不呢?这种特定情况有解决方法吗?(提示:你知道输出数组的大小,所以可以从返回的double *构造一个Numpy数组。

2.8.8.4. Cython

  1. 看看Cython自动生成的代码。仔细看看Cython插入的一些注释。你看到了什么?
  2. 看看Cython文档中的使用Numpy部分,了解如何逐步优化使用Numpy的纯Python脚本。
  3. 修改Numpy示例,使cos_doubles_func处理您的预分配,从而使它更像Numpy-C-API示例。