2.8 与C进行交互

In [1]:

%matplotlib inline
import numpy as np

作者: Valentin Haenel

本章包含了许多可以在Python使用原生代码(主要是C/C++)方式的介绍,这个过程通常被称为封装。本章的目的是给你有哪些技术存在已经各自有优劣式的一点儿感觉,这样你可以根据你的具体需求选择适合的方式。无论如何,只要你开始做封装,你几乎都必然需要咨询你选定技术的文档。

章节内容

简介
Python-C-Api
Ctypes
SWIG
Cython
总结
进一步阅读和参考
练习

2.8.1 简介

本章将涵盖一下技术:

  • Python-C-Api
  • Ctypes
  • SWIG (简化封装器和接口生成器)
  • Cython

这四种技术可能是最知名的,其中Cython可能是最高级的,并且你应该最优先使用它。其他的技术也很重要,如果你想要从不同点角度理解封装问题。之前提到过,还有其他的替代技术,但是,理解了以上这些基础的,你就可以评估你选择的技术是否满足你的需求。

在评估技术时,下列标准会有帮助:

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

在你动手前,应该先考虑一下使用情景。在于原生代码交互时,通常来自于两个应用场景:

  • 需要利用C/C++中现存的代码,或者是因为它已经存在,或者是因为它更快。
  • Python代码太慢,将内部循环变成原生代码

每个技术都使用来自math.h的cos函数的封装来进行演示。尽管这是一个无聊例子,但是它确实给我们很好的演示了封装方法的基础,因为每个技术也包含了一定程度Numpy支持,这也用计算一些数组来计算consine的例子来演示。

最后,两个小警示:

  • 所有这些方法在Python解释器中都可能崩溃(细分错误),因为在C代码中的错误。
  • 所有的例子都在Linux中完成,他们应该在其他操作系统中也可用。
  • 在大多数例子中,你都会需要C编译器。

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 [ ]:

/*  用Python-C-API封装来自math.h的cos函数的例子 */

#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);
}

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

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

In [ ]:

from distutils.core import setup, Extension

# 定义扩展模块
cos_module = Extension('cos_module', sources=['cos_module.c'])

# 运行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 [ ]:

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 [ ]:

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 支持

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 [ ]:

/*  使用Numpy-C-API封装来自math.h的cos函数 . */

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

/*  封装cosine函数 */
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;
}

/*  在模块中定义函数 */
static PyMethodDef CosMethods[] =
{
     {"cos_func_np", cos_func_np, METH_VARARGS,
         "evaluate the cosine on a numpy array"},
     {NULL, NULL, 0, NULL}
};

/* 模块初始化 */
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头部:

In [ ]:

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])

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

In [ ]:

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()

这会产生以下的图像:

2.8.3. Ctypes

Ctypes是Python的一个外来函数库。它提供了C兼容的数据类型,并且允许在DLLs或者共享的库中调用函数。它可以用来在纯Python中封装这些库。

优点

  • Python标准库的一部分
  • 不需要编译
  • 代码封装都是在Python中

不足

  • 需要代码作为一个共享的库(粗略地说,在windows中是 .dll,在Linux中是.so,在Mac OSX中是 *.dylib)
  • 对C++支持并不好

2.8.3.1 例子

如前面提到的,代码封装完全在Python中。

In [ ]:

""" 用ctypes封装来自math.h的 cos 函数。 """

import ctypes
from ctypes.util import find_library

# find and load the library
libm = ctypes.cdll.LoadLibrary(find_library('m'))
# set the argument type
libm.cos.argtypes = [ctypes.c_double]
# set the return type
libm.cos.restype = ctypes.c_double

def cos_func(arg):
    ''' 封装math.h cos函数 '''
    return libm.cos(arg)
  • 寻找和加载库可能非常依赖于你的操作系统,检查文档来了解细节
  • 这可能有些欺骗性,因为math库在系统中已经是编译模式。如果你想要封装一个内置的库,需要先编译它,可能需要或者不需要额外的工作。 我们现在可以像前面一样使用这个库:

In [ ]:

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

2.8.3.2 Numpy支持

Numpy包含一些与ctypes交互的支持。特别是支持将特定Numpy数组属性作为ctypes数据类型研究,并且有函数可以将C数组和Numpy数据互相转换。

更多信息,可以看一下Numpy手册的对应部分或者numpy.ndarray.ctypesnumpy.ctypeslib的API文档。

对于下面的例子,让我们假设一个C函数,输入输出都是一个数组,计算输入数组的cosine并将结果输出为一个数组。

库包含下列头文件(尽管在这个例子中并不是必须这样,为了完整,我们还是把这一步列出来):

In [ ]:

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

这个函数实现在下列的C源文件中:

In [ ]:

#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的组合:

In [ ]:

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

接下来,我们可以将这个库编译到共享的库 (on Linux)libcos_doubles.so:

In [ ]:

$ 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数组的直接支持(一定程度上)来封装这个库:

In [ ]:

""" 封装一个使用numpy.ctypeslib接受C双数组作为输入的例子。"""

import numpy as np
import numpy.ctypeslib as npct
from ctypes import c_int

# cos_doubles的输入类型
# 必须是双数组, 有相邻的单维度
array_1d_double = npct.ndpointer(dtype=np.double, ndim=1, flags='CONTIGUOUS')

# 加载库,运用numpy机制
libcd = npct.load_library("libcos_doubles", ".")

# 设置反馈类型和参数类型
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最终的cos_doubles_func需要两个Numpy数组作为参数。

并且,和前面一样,我们我要为自己证明一下它是有效的:

In [ ]:

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()

2.8.4 SWIG

SWIG, 简化封装接口生成器,是一个联接用C和C++写的程序与需要高级程序语言,包括Python的软件开发工具。SWIG的重点在于它可以为你自动生成封装器代码。尽管从编程时间上来说这是一个优势,但是同时也是一个负担。生成的文件通常很大,并且可能并不是人类可读的,封装过程造成的多层间接引用可能很难理解。

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

优势

  • 给定头部可以自动封装整个库
  • 在C++中表现良好

不足

  • 自动生成的文件很庞大
  • 如果出错很难debug
  • 陡峭的学习曲线

2.8.4.1 例子

让我们想象我们的cos函数存在于用C写的cos_module中,包含在源文件cos_module.c中:

In [ ]:

#include <math.h>

double cos_func(double arg){
    return cos(arg);
}

头文件cos_module.h:

In [ ]:

double cos_func(double arg);

尽管我们的目的是将cos_func暴露给Python。要用SWIG来完成这个目的,我们需要写一个包含SWIG指导的接口文件。

In [ ]:

/*  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确实允许更多精细包含或者排除在头文件中发现的函数,细节检查一下文档。

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

  • 在接口文件上运行swig可执行文件来生成文件cos_module_wrap.c, 其源文件是自动生成的Python C-extension和cos_module.py, 是自动生成的Python模块。

  • 编译cos_module_wrap.c_cos_module.so。幸运的,distutils知道如何处理SWIG接口文件, 因此我们的setup.py是很简单的:

In [ ]:

from distutils.core import setup, Extension

setup(ext_modules=[Extension("_cos_module",
      sources=["cos_module.c", "cos_module.i"])])

In [ ]:

$ 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 [ ]:

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 [ ]:

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 支持

Numpy在numpy.i文件中提供了SWIG的支持。这个接口文件定义了许多所谓的typemaps,支持了Numpy数组和C-Arrays的转化。在接下来的例子中,我们将快速看一下typemaps实际是如何工作的。

我们有相同的cos_doubles函数,在ctypes例子中:

In [ ]:

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

In [ ]:

#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:

In [ ]:

/*  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的typemaps, 我们需要包含numpy.i文件。
  • 观察一下对import_array()的调用,这个模块我们已经在Numpy-C-API例子中遇到过。
  • 因为类型映射只支持ARRAY、SIZE的签名,我们需要将cos_doubles封装为cos_doubles_func,接收两个数组包括大小作为输入。
  • 与SWIG不同的是, 我们并没有包含cos_doubles.h头部,我们并不需要暴露给Python,因为,我们通过cos_doubles_func暴露了相关的功能。

并且,和之前一样,我们可以用distutils来封装这个函数:

In [ ]:

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来制定位置。

In [ ]:

$ 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

并且,和前面一样,我们来验证一下它工作正常:

In [ ]:

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()

2.8.5 Cython

Cython既是写C扩展的类Python语言,也是这种语言的编译器。Cython语言是Python的超集,带有额外的结构,允许你调用C函数和C类型的注释变量和类属性。在这个意义上,可以称之为带有类型的Python。

除了封装原生代码的基础应用案例,Cython也支持额外的应用案例,即交互优化。从根本上来说,从纯Python脚本开始,向瓶颈代码逐渐增加Cython类型来优化那些确实有影响的代码。

在这种情况下,与SWIG很相似,因为代码可以自动生成,但是,从另一个角度来说,又与ctypes很类似,因为,封装代码(大部分)是用Python写的。

尽管其他自动生成代码的解决方案很难debug(比如SWIG),Cython有一个GNU debugger扩展来帮助debug Python,Cython和C代码。

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

优点

  • 类Python语言来写扩展
  • 自动生成代码
  • 支持增量优化
  • 包含一个GNU debugger扩展
  • 支持C++ (从版本0.13) 不足
  • 必须编译
  • 需要额外的库 ( 只是在build的时候, 在这个问题中,可以通过运送生成的C文件来克服)

2.8.5.1 例子

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

In [ ]:

""" 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的更多代码:

In [ ]:

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"])]
)

编译这个模块:

In [ ]:

$ 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 [ ]:

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 [ ]:

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 math库的声明,上面的代码可以简化为:

In [ ]:

""" 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支持

Cython通过numpy.pyx文件支持Numpy,允许你为你的Cython代码添加Numpy数组类型,即就像指定变量iint类型,你也可以指定变量a是带有给定的dtypenumpy.ndarray。同时,同时特定的优化比如边际检查也是支持的。看一下Cython文档的对应部分。如果你想要将Numpy数组作为C数组传递给Cython封装的C函数,在Cython wiki上有对应的部分。

在下面的例子中,我们将演示如何用Cython来封装类似的cos_doubles

In [ ]:

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

In [ ]:

#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:

In [ ]:

""" 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来编译:

In [ ]:

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选项。

In [ ]:

$ 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

和前面一样,我们来验证一下它是有效的:

In [ ]:

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()

2.8.6 总结

这个部分演示了四种与原生代码交互的技术。下表概述了这些技术的一些方面。

x Part of CPython Compiled Autogenerated Numpy Support
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 Further Reading and References

Gaël Varoquaux关于避免数据复制的博客给出了一些如何精明的处理内存管理的见解。如果你在大数据量时出现问题,可以回到这里寻找一些灵感。

2.8.8 练习

因为这是一个新部分,练习更像是一个接下来应该查看什么的指示器,因此,看一下那些你觉得更有趣的部分。如果你有关于练习更好点子,请告诉我们!

  • 下载每个例子的源码,并且在你的机器上运行并编译他们。
  • 对每个例子做一些修改,并且自己验证一下是否有效。 ( 比如,将cos改为sin。)
  • 绝大多数例子,特别是包含了Numpy的例子,可能还是比较脆弱,对输入错误反应较差。找一些方法来让例子崩溃,找出问题所在,并且设计潜在的解决方案。这些是有些点子:

    • 数字溢出
    • 输入输出数组长度不一致
    • 多维度数据
    • 空数组
    • non-double类型数组
  • 使用%timeitIPython魔法函数来测量不同解决方案的执行时间

2.8.8.1 Python-C-API

  • 修改Numpy例子以便函数有两个输入参数,第二个参数是预分配输出数组,让它与其他的Numpy例子一致。
  • 修改这个例子,以便这个函数只有一个输入数组,在原地修改这个函数。
  • 试着用新的Numpy迭代协议修改例子。如果你刚好获得了一个可用的解决方案,请将其提交一个请求到github。
  • 你可能注意到了,Numpy-C-API例子只是Numpy例子没有封装cos_doubles但是直接将cos函数应用于Numpy数组的元素上。这样做与其他技术相比有什么优势。
  • 你可以只用Numpy-C-API来封装cos_doubles。你可能需要确保数组有正确的类型,并且是单维度和内存临近。

2.8.8.2 Ctypes

  • 修改Numpy例子以便cos_doubles_func为你处理预分配,让它更像Numpy-C-API例子。

2.8.8.3. SWIG

  • 看一下SWIG自动生成的代码,你能理解多少?
  • 修改Numpy例子,以便cos_doubles_func为你处理预处理,让它更像Numpy-C-API例子。
  • 修改cos_doubles C 函数,以便它返回分配的数组。你可以用SWIG typemaps类封装吗? 如果不可以,为什么不可以? 对于这种特殊的情况有没有什么变通方法? (提示: 你知道输出数组的大小, 因此,可以从返回的double \*构建Numpy数组。

2.8.8.4 Cython

  • 看一下Cython自动生成的代码。仔细看一下Cython插入的一些评论。你能看到些什么?
  • 看一下Cython文档中与Numpy工作的部分, 学习一下如何使用Numpy增量优化python脚本。
  • 修改Numpy例子,以便cos_doubles_func为你处理预处理,让它更像Numpy-C-API例子。