Python 中使用 C 代碼:以 Numpy 爲例

這個章節包含許多在python代碼中支持c/c++本機代碼的許多不一樣方法, 一般這個過程叫做包裹(wrapping)。本章的目的是讓您大體知道有那些技術和它們分別的優缺點是什麼,因而您可以爲您本身的特定須要選擇什麼時候的技術。在任何狀況下,一旦您開始包裹,您幾乎必定將想要查閱您所選技術各自的文檔。html

簡介

本章節包含如下技術:python

這四種方法大概是最著名的,其中Cython多是最高級且應該優先使用的。若是您想從其它角度理解包裹問題,其它方法也很重要。已經說過,雖然還有其它方法,可是理解以上基本方法,您將能評估您本身的選擇看是否符合本身的須要。linux

如下標準在評估一項技術時也許有用:c++

  • 是否須要額外的庫?
  • 代碼是不是自動生成的?
  • 須要編譯嗎?
  • 和Numpy數組進行交互方便嗎?
  • 支持C++嗎?

首先,您應該考慮你的用例。當用本機代碼接口時,一般有兩個用例:git

  • 存在須要充分利用的C/C++代碼,或者那些代碼已經存在,或者那些代碼更快。
  • Python代碼慢爆了,將內循環交給本機代碼處理

每一個技術經過包裹math.h中的cos函數實現。儘管這是微不足道的例子,它將很好的展現基本的包裹問題。由於每一個技術也包括某種形式的Numpy支持,這也經過使用一個餘弦函數被在某種數組上計算的例子來展現。github

最後但重要的是兩個小警告:編程

  • 全部這些技術均可能形成Python解釋器崩潰(段錯誤),這(一般)是C代碼的Bug。
  • 全部例子在linux上完成,也應該能在其它操做系統上實現
  • 大多數例子中你須要一個C編譯器

Python-C-Api

Python-C-API是標準Python解釋器(就是所謂的CPython)的支柱。使用這個API能夠用C或C++語言編寫Python擴展。顯然這些擴展模塊能夠憑藉語言兼容性,調用任何C或C++寫成的函數。segmentfault

當使用Python-C-API時,人們一般寫許多樣板代碼,先解析傳遞給函數的參數,而後構建並返回類型。api

優勢數組

  • 無需額外的庫
  • 許多低級的控制
  • 徹底能夠用C++

劣勢

  • 可能須要大量工做
  • 代碼中的大量開銷
  • 必須編譯
  • 高額的維護代價
  • 當跨Python版本時若C-Api變化沒有後向兼容性

注意:如下Python-C-Api示例主要爲了展現須要。由於大多其它技術實際上依賴這個,因此最好對它如何工做有個高層次的瞭解。在99%的用例中你最好使用其它技術。

示例

如下C擴展模塊,讓標準數學庫中的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])

這能被編譯:

~/Work/scipy-lecture-notes/interfacing-with-c $ls
cos_module.c  setup.py
 ~/Work/scipy-lecture-notes/interfacing-with-c $python setup.py build_ext --inplace
running build_ext
building 'cos_module' extension
x86_64-pc-linux-gnu-gcc -pthread -fPIC -I/usr/include/python2.7 -c cos_module.c -o build/temp.linux-x86_64-2.7/cos_module.o
x86_64-pc-linux-gnu-gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o -L/usr/lib64 -lpython2.7 -o /home/lyy/Work/scipy-lecture-notes/interfacing-with-c/cos_module.so
 ~/Work/scipy-lecture-notes/interfacing-with-c $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/lyy/Work/scipy-lecture-notes/interfacing-with-c/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[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')

TypeError: a float is required

Numpy支持

相似於Python-C-API,Numpu自身做爲C擴展實現也有Numpy-C-API。這個API能夠在寫自定的C擴展時,被用來從C建立和操做Numpy數組。參見高級Numpy

如下例子展現瞭如何將Numpy數組做爲參數傳遞給函數,如何使用(老的)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;
    PyArrayIterObject *in_iter;
    PyArrayIterObject *out_iter;

    /*  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 */
    /* TODO: this iterator API is deprecated since 1.6
     *       replace in favour of the new NpyIter API */
    in_iter  = (PyArrayIterObject *)PyArray_IterNew((PyObject*)in_array);
    out_iter = (PyArrayIterObject *)PyArray_IterNew(out_array);
    if (in_iter == NULL || out_iter == NULL)
        goto fail;

    /*  iterate over the arrays */
    while (in_iter->index < in_iter->size
            && out_iter->index < out_iter->size) {
        /* get the datapointers */
        double * in_dataptr = (double *)in_iter->dataptr;
        double * out_dataptr = (double *)out_iter->dataptr;
        /* cosine of input into output */
        *out_dataptr = cos(*in_dataptr);
        /* update the iterator */
        PyArray_ITER_NEXT(in_iter);
        PyArray_ITER_NEXT(out_iter);
    }

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

    /*  in case bad things happen */
    fail:
        Py_XDECREF(out_array);
        Py_XDECREF(in_iter);
        Py_XDECREF(out_iter);
        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編譯這個。然而,咱們必須經過使用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()

結果將以下圖

Ctypes

Ctypes是一個Python的外部函數庫。它提供了兼容C的數據類型。而且容許調用DLL或共享庫中的函數。它可以被用來將這些庫用純Python包裹。

優點

  • Python標準庫的一部分
  • 沒必要編譯
  • 徹底用Python包裹代碼

劣勢

  • 須要將代碼包裹做爲共享庫得到(粗略地說就是Windows中的*.dll、Linux下的*.so和Mac OSX的*.dylib)
  • 對C++支持很差

示例

如上所述,包裹的代碼是純Python的。

""" Example of wrapping cos function from math.h using ctypes. """

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):
    ''' 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

Numpy支持

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

咱們接着能夠將之編譯到共享庫libcos_double.so中(linux下):

$ 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 typs 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函數要求這種緩衝區。^1^
  • 注意輸出數組必須預先分配,例如經過numpy.zeros(),這個函數將寫進它的緩衝區。
  • 儘管cos_doubles函數的原始參數是ARRAY, ARRAY, int,最終的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()

SWIG

SWIG, 簡化包裹接口生成器,是一個將不一樣高級編程語言包括Python連接到用C和C++寫的程序上的軟件開發工具。SWIG重要的功能是,它能自動爲你生成包裹代碼。這就開發時間來講是個優點,也多是個負擔。生成文件趨於巨大,讀起來不友好,包裹過程的結果就是多個間接層,可能有點難以理解。

注意:自動生成的C代碼使用Python-C-Api。

優點

  • 能夠自動包裹給定頭文件的整個庫
  • 對C++工做很好

劣勢

  • 自動生成巨大的文件
  • 若出錯難以調試
  • 陡峭的學習曲線

示例

讓咱們假設咱們的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是自動生成的純Python模塊。
  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'

Numpy支持

numpy經過numpy.i文件提供了對SWIG的支持。這個接口文件定義了各類所謂的類型映射(typemaps)來轉換Numpy數組和C數組。在下面的例子中咱們將簡略地看看這種類型映射在實際中如何起做用。

咱們使用在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文件。
  • 觀察import_array()的調用,咱們已經在Numpy-C-Api的例子中見到過。
  • 由於類型映射僅僅支持參數ARRAY, SIZE咱們須要包裹cos_doublescos_doubles_func,該函數接受兩個數組包含各自大小做爲輸入。
  • 相對於簡單SWIG的例子,咱們不須要包含cos_doubles.h頭文件,由於咱們經過cos_doubles_func暴露這個功能,咱們沒有其它東西想暴露給Python。

而後,如前述用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()

Cython

Cython不只使用來寫C擴展的Python樣子的語言,並且是這個語言的一個高級編譯器。Cython語言是Python的超集,包含額外的結構容許你調用C函數,將變量和類屬性解釋爲C類型。在這個意義上能夠叫它Python的一個類型。

除了這些幾本的包裹原生代碼的用例,Cython支持一個額外的用例,即交互優化。基本上是,從純Python代碼腳本出發逐步向代碼瓶頸增長Cython類型來優化那些真正值得優化的代碼。

在這個意義上它和SWIG很是類似,由於C代碼能夠自動生成,但某種意義上它也至關相似與ctypes,由於它包裹代碼能夠(幾乎能夠)用Python寫成。

儘管其它自動生成代碼方案會很難調試(例如SWIG),Cython帶有一個GNU調試器的擴展,能幫助調試Python,Cython和C代碼。

注意:自動生成的C代碼使用了Python-C-Api。

優點

  • 類Python的語言來寫C擴展
  • 自動生成代碼
  • 支持增量優化
  • 包含一個GNU調試器擴展
  • 支持C++(自從0.13版本)

劣勢

  • 必須編譯
  • 須要額外的庫(但僅僅在編譯時,這個問題能夠經過傳遞一個生成的C文件克服)

示例

咱們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)

注意額外的關鍵字像cdefexterncos_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聲明被用來importcos函數。

Numpy支持

Cython經過numpy.pyx文件支持Numpy,這容許你將Numpy數組類型添加到Cython代碼。例如將i指定爲int類型,將變量a指定爲numpy.ndarray並給定dtype。某些優化像邊界檢查也支持。參看Cython文檔的相關章節。萬一你想將Numpy數組做爲C數組傳遞給你的Cython包裹的C代碼,Cython維基中有一個章節。

在如下例子中,咱們將展現如何如何使用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 """

# import both numpy and the Cython declarations for numpy
import numpy as np
cimport numpy as np

# if you want to use the Numpy-C-API from Cython
# (not strictly necessary for this example)
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()

總結

這個章節中四種不一樣和本地代碼接口技術被呈如今您面前。這個表格簡要的總結了這些技術的某些方面。

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代碼中添加類型增量優化代碼的能力是獨一無二的。

更多閱讀和參考

練習

因這是一個全新的章節,這些練習更可視爲是接下來看什麼的指針。因此選擇您最感興趣的那個。若是你對此有更多好的想法,請聯繫咱們!

  1. 下載每一個練習的源碼並在你的機器上編譯運行。
  2. 對每一個例子作小的修改確信它能起做用。(像將cos改爲sin)
  3. 大多數例子,特別是涉及Numpy的例子可能仍然對錯誤輸入很脆弱、而且返回模糊的消息。尋找使這些例子出問題的方法,指出問題是什麼而且設計潛在的解決方案。這有一些提示:

    1. 數值溢出
    2. 輸入輸出數組長度不一樣
    3. 多維數組
    4. 空數組
    5. double型數組
  4. 使用IPython中的magic%timeit來測量不一樣方案的執行時間。

Python-C-Api

  1. 更改Numpy的例子讓函數接受兩個輸入參數,第二個參數是預分配的輸出數組,讓它像其它Numpy例子。
  2. 更改例子讓函數僅僅接受一個輸入數組而且原地修改。
  3. 嘗試使用Numpy迭代協議修正例子,若是你設法獲取了一個工做的解決方案,請在github上發佈一個拉取請求(pull-request)。
  4. 你也許注意到了,Numpy-C-API例子是惟一不包裹cos_double可是直接將cos應用到Numpy數組的元素的Numpy例子。這相對於其它技術有什麼優點?
  5. 你能僅僅使用NJumpy-C-API包裹cos_doubles嗎?你可能須要確保數組是正確的類型,而且在內存中一維連續。

Ctypes

  1. 更改像cos_double_func的Numpy例子爲你處理預分配,使之更像Numpy-C-Api例子。

SWIG

  1. 查看SWIG自動生成的代碼,你能理解多少?
  2. 更改Numpy的例子像cos_double_func處理預分配,讓它更像Numpy-C-API的例子。
  3. 更改C函數cos_doubles讓它返回一個分配的數組。你能用SWIG類型映射包裹它?若是不能,爲什麼不行?有沒有特定條件的變通方案。(提示:你知道輸出數組的大小,因此可能從返回的double *構建一個Numpy數組。)

Cython

  1. 查看Cython自動生成的代碼。仔細看看Cython插入的一些註釋。你看到了什麼?
  2. 查看Cython文檔的章節Working with
    Numpy
    去學習使用Numpy如何增量優化純python代碼。
  3. 更改Numpy例子好比cos_doubles_func處理預分配,使之更像Numpy-C-Api例子。

原文 scipy lecture notes: Interfacing with C
翻譯 reverland under GNU Free Documentation License 1.2.

相關文章
相關標籤/搜索