2.8. 与 C 接口

作者: Valentin Haenel

本章包含了简介,介绍了从 Python 访问原生代码(主要是C/C++)的多种不同途径,这个过程通常被称为封装。本章的目标是让你了解现有的技术及其各自的优缺点,以便你可以根据自己的具体需求选择合适的技术。无论如何,一旦你开始封装,你几乎肯定会需要查阅你所选技术的相关文档。

2.8.1. 简介

本章涵盖以下技术

这四种技术可能是最知名的技术,其中 Cython 可能是最先进的技术,你应该首先考虑使用它。如果你想从不同的角度理解封装问题,其他的技术也很重要。话虽如此,还有其他的选择,但是理解了上面几种技术的原理之后,你就可以评估你选择的技术是否符合你的需求。

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

  • 是否需要额外的库?

  • 代码是否自动生成?

  • 它是否需要编译?

  • 它是否对与 NumPy 数组交互有良好的支持?

  • 它是否支持 C++?

在你开始之前,你应该考虑你的用例。当与原生代码交互时,通常会遇到两种用例

  • 需要利用现有的 C/C++ 代码,要么是因为它已经存在,要么是因为它更快。

  • Python 代码太慢,将内部循环推送到原生代码

每种技术都通过封装 cos 函数(来自 math.h)来演示。虽然这只是一个很简单的例子,但它应该足以帮助我们演示封装解决方案的基础知识。由于每种技术都包含某种形式的 NumPy 支持,因此这也使用一个示例来演示,其中余弦函数是在某种数组上计算的。

最后但并非最不重要的是,有两个小警告

  • 所有这些技术都可能导致 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++ 使用

缺点

  • 可能需要大量的努力

  • 代码中有很多开销

  • 必须编译

  • 维护成本高

  • 跨 Python 版本没有向前兼容性,因为 C-API 会发生变化

  • 引用计数错误很容易创建,也很难跟踪。

注意

这里的 Python-C-Api 示例主要用于教学目的。许多其他技术实际上依赖于它,因此了解它如何工作是很有用的。在 99% 的用例中,你会更好地使用其他技术。

注意

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

2.8.2.1. 示例

以下 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}
};
#if PY_MAJOR_VERSION >= 3
/* module initialization */
/* Python version 3*/
static struct PyModuleDef cModPyDem =
{
PyModuleDef_HEAD_INIT,
"cos_module", "Some documentation",
-1,
CosMethods
};
PyMODINIT_FUNC
PyInit_cos_module(void)
{
return PyModule_Create(&cModPyDem);
}
#else
/* module initialization */
/* Python version 2 */
PyMODINIT_FUNC
initcos_module(void)
{
(void) Py_InitModule("cos_module", CosMethods);
}
#endif

正如你所看到的,有很多样板代码,既要将参数和返回类型“整理”到位,也要用于模块初始化。虽然其中一些是摊销的,但随着扩展的增长,每个函数(或函数组)所需的样板代码仍然存在。

标准 Python 构建系统 setuptools 通过 setup.py 文件支持编译 C 扩展

from setuptools 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/scientific-python-lectures/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 解释器中加载它

注意

在 Python 3 中,编译模块的文件名包含有关 Python 解释器的元数据(参见PEP 3149),因此更长。导入语句不受此影响。

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/scientific-python-lectures/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[6]: -1.0

现在让我们看看它有多健壮

In [7]: 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 支持

与 Python-C-API 类似,NumPy 本身是用 C 扩展实现的,它带有NumPy-C-API。这个 API 可以用来在编写自定义 C 扩展时从 C 创建和操作 NumPy 数组。另见:高级 NumPy

注意

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

以下示例展示了如何将 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 *arrays[2]; /* holds input and output array */
PyObject *ret;
NpyIter *iter;
npy_uint32 op_flags[2];
npy_uint32 iterator_flags;
PyArray_Descr *op_dtypes[2];
NpyIter_IterNextFunc *iternext;
/* parse single NumPy array argument */
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &arrays[0])) {
return NULL;
}
arrays[1] = NULL; /* The result will be allocated by the iterator */
/* Set up and create the iterator */
iterator_flags = (NPY_ITER_ZEROSIZE_OK |
/*
* Enable buffering in case the input is not behaved
* (native byte order or not aligned),
* disabling may speed up some cases when it is known to
* be unnecessary.
*/
NPY_ITER_BUFFERED |
/* Manually handle innermost iteration for speed: */
NPY_ITER_EXTERNAL_LOOP |
NPY_ITER_GROWINNER);
op_flags[0] = (NPY_ITER_READONLY |
/*
* Required that the arrays are well behaved, since the cos
* call below requires this.
*/
NPY_ITER_NBO |
NPY_ITER_ALIGNED);
/* Ask the iterator to allocate an array to write the output to */
op_flags[1] = NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE;
/*
* Ensure the iteration has the correct type, could be checked
* specifically here.
*/
op_dtypes[0] = PyArray_DescrFromType(NPY_DOUBLE);
op_dtypes[1] = op_dtypes[0];
/* Create the NumPy iterator object: */
iter = NpyIter_MultiNew(2, arrays, iterator_flags,
/* Use input order for output and iteration */
NPY_KEEPORDER,
/* Allow only byte-swapping of input */
NPY_EQUIV_CASTING, op_flags, op_dtypes);
Py_DECREF(op_dtypes[0]); /* The second one is identical. */
if (iter == NULL)
return NULL;
iternext = NpyIter_GetIterNext(iter, NULL);
if (iternext == NULL) {
NpyIter_Deallocate(iter);
return NULL;
}
/* Fetch the output array which was allocated by the iterator: */
ret = (PyObject *)NpyIter_GetOperandArray(iter)[1];
Py_INCREF(ret);
if (NpyIter_GetIterSize(iter) == 0) {
/*
* If there are no elements, the loop cannot be iterated.
* This check is necessary with NPY_ITER_ZEROSIZE_OK.
*/
NpyIter_Deallocate(iter);
return ret;
}
/* The location of the data pointer which the iterator may update */
char **dataptr = NpyIter_GetDataPtrArray(iter);
/* The location of the stride which the iterator may update */
npy_intp *strideptr = NpyIter_GetInnerStrideArray(iter);
/* The location of the inner loop size which the iterator may update */
npy_intp *innersizeptr = NpyIter_GetInnerLoopSizePtr(iter);
/* iterate over the arrays */
do {
npy_intp stride = strideptr[0];
npy_intp count = *innersizeptr;
/* out is always contiguous, so use double */
double *out = (double *)dataptr[1];
char *in = dataptr[0];
/* The output is allocated and guaranteed contiguous (out++ works): */
assert(strideptr[1] == sizeof(double));
/*
* For optimization it can make sense to add a check for
* stride == sizeof(double) to allow the compiler to optimize for that.
*/
while (count--) {
*out = cos(*(double *)in);
out++;
in += stride;
}
} while (iternext(iter));
/* Clean up and return the result */
NpyIter_Deallocate(iter);
return ret;
}
/* 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}
};
#if PY_MAJOR_VERSION >= 3
/* module initialization */
/* Python version 3*/
static struct PyModuleDef cModPyDem = {
PyModuleDef_HEAD_INIT,
"cos_module", "Some documentation",
-1,
CosMethods
};
PyMODINIT_FUNC PyInit_cos_module_np(void) {
PyObject *module;
module = PyModule_Create(&cModPyDem);
if(module==NULL) return NULL;
/* IMPORTANT: this must be called */
import_array();
if (PyErr_Occurred()) return NULL;
return module;
}
#else
/* module initialization */
/* Python version 2 */
PyMODINIT_FUNC initcos_module_np(void) {
PyObject *module;
module = Py_InitModule("cos_module_np", CosMethods);
if(module==NULL) return;
/* IMPORTANT: this must be called */
import_array();
return;
}
#endif

为了编译它,我们可以再次使用 setuptools。但是,我们需要确保通过使用numpy.get_include() 包含 NumPy 头文件。

from setuptools 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 matplotlib.pyplot as plt
x = np.arange(0, 2 * np.pi, 0.1)
y = cos_module_np.cos_func_np(x)
plt.plot(x, y)
plt.show()
# Below are more specific tests for less common usage
# ---------------------------------------------------
# The function is OK with `x` not having any elements:
x_empty = np.array([], dtype=np.float64)
y_empty = cos_module_np.cos_func_np(x_empty)
assert np.array_equal(y_empty, np.array([], dtype=np.float64))
# The function can handle arbitrary dimensions and non-contiguous data.
# `x_2d` contains the same values, but has a different shape.
# Note: `x_2d.flags` shows it is not contiguous and `x2.ravel() == x`
x_2d = x.repeat(2)[::2].reshape(-1, 3)
y_2d = cos_module_np.cos_func_np(x_2d)
# When reshaped back, the same result is given:
assert np.array_equal(y_2d.ravel(), y)
# The function handles incorrect byte-order fine:
x_not_native_byteorder = x.astype(x.dtype.newbyteorder())
y_not_native_byteorder = cos_module_np.cos_func_np(x_not_native_byteorder)
assert np.array_equal(y_not_native_byteorder, y)
# The function fails if the data type is incorrect:
x_incorrect_dtype = x.astype(np.float32)
try:
cos_module_np.cos_func_np(x_incorrect_dtype)
assert 0, "This cannot be reached."
except TypeError:
# A TypeError will be raised, this can be changed by changing the
# casting rule.
pass

这应该得到以下图形

../../_images/test_cos_module_np.png

2.8.3. Ctypes

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

优点

  • Python 标准库的一部分

  • 不需要编译

  • 用纯 Python 包装代码

缺点

  • 需要将要包装的代码作为共享库提供(粗略地说,在 Windows 中是 *.dll,在 Linux 中是 *.so,在 Mac OSX 中是 *.dylib)。

  • 对 C++ 没有很好的支持

2.8.3.1. 示例

如前所述,包装代码是用纯 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_name = find_library("m")
assert libm_name is not None, "Cannot find libm (math) on this system :/ That's bad."
libm = ctypes.cdll.LoadLibrary(libm_name)
# 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 [8]: import cos_module
In [9]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scientific-python-lectures/advanced/interfacing_with_c/ctypes/cos_module.py
Docstring: <no docstring>
In [10]: dir(cos_module)
Out[10]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'cos_func',
'ctypes',
'find_library',
'libm']
In [11]: cos_module.cos_func(1.0)
Out[11]: 0.5403023058681398
In [12]: cos_module.cos_func(0.0)
Out[12]: 1.0
In [13]: cos_module.cos_func(3.14159265359)
Out[13]: -1.0

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

In [14]: 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/scientific-python-lectures/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 支持

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,我们不能使用 setuptools 编译它,而必须使用 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,但最终的 cos_doubles_func 只接收两个 NumPy 数组作为参数。

并且,与之前一样,我们说服自己它有效

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

2.8.4. SWIG

SWIG,即简化包装器接口生成器,是一种软件开发工具,它可以将用 C 和 C++ 编写的程序与多种高级编程语言(包括 Python)连接起来。SWIG 的重要之处在于它可以自动为您生成包装代码。虽然这在开发时间方面是一个优势,但也可能成为一种负担。生成的代码文件往往相当大,可能不太易读,而且由于包装过程导致的多级间接性可能难以理解。

注意

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

优点

  • 可以根据头文件自动包装整个库

  • 与 C++ 完美配合

缺点

  • 自动生成巨大的文件

  • 如果出现错误,则难以调试

  • 学习曲线陡峭

2.8.4.1. 示例

假设我们的 cos 函数位于一个名为 cos_module 的模块中,该模块是用 c 编写的,包含源文件 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。幸运的是,setuptools 知道如何处理 SWIG 接口文件,因此我们的 setup.py 很简单

from setuptools 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/scientific-python-lectures/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 [15]: import cos_module
In [16]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scientific-python-lectures/advanced/interfacing_with_c/swig/cos_module.py
Docstring: <no docstring>
In [17]: dir(cos_module)
Out[17]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'_cos_module',
'_newclass',
'_object',
'_swig_getattr',
'_swig_property',
'_swig_repr',
'_swig_setattr',
'_swig_setattr_nondynamic',
'cos_func']
In [18]: cos_module.cos_func(1.0)
Out[18]: 0.5403023058681398
In [19]: cos_module.cos_func(0.0)
Out[19]: 1.0
In [20]: cos_module.cos_func(3.14159265359)
Out[20]: -1.0

再次测试其健壮性,我们会看到我们获得了更好的错误消息(尽管从严格意义上讲,Python 中没有 double 类型)

In [21]: 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 提供支持。该接口文件定义了各种所谓的类型映射,这些映射支持 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 function, 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_doubles 包装为 cos_doubles_func,它将包括大小在内的两个数组作为输入。

  • 与简单的 SWIG 示例相反,我们不包含 cos_doubles.h 头文件。因为我们想通过 cos_doubles_func 暴露功能,所以头文件里没有我们要暴露给 Python 的内容。

和以前一样,我们可以使用 setuptools 来包装它

from setuptools 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/scientific-python-lectures/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 matplotlib.pyplot as plt
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
plt.plot(x, y)
plt.show()
../../_images/test_cos_doubles1.png

2.8.5. Cython

Cython 既是一种用于编写 C 扩展的类似 Python 的语言,也是一种用于编译此语言的先进编译器。Cython 语言是 Python 的超集,它带有额外的构造,允许您调用 C 函数并使用 c 类型来注释变量和类属性。从这个意义上说,也可以称它为带类型的 Python

除了包装本机代码的基本用例之外,Cython 还支持另一种用例,即交互式优化。基本上,您从纯 Python 脚本开始,逐渐将 Cython 类型添加到瓶颈代码中,以优化真正重要的那些代码路径。

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

虽然其他自动生成代码的解决方案可能很难调试(例如 SWIG),但 Cython 带有一个 GNU 调试器扩展,可以帮助调试 Python、Cython 和 C 代码。

注意

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

优点

  • 用于编写 C 扩展的类似 Python 的语言

  • 自动生成的代码

  • 支持增量优化

  • 包含 GNU 调试器扩展

  • 支持 C++(从 0.13 版开始)

缺点

  • 必须编译

  • 需要一个额外的库(但只在构建时需要,这个问题可以通过发布生成的 C 文件来解决)

2.8.5.1. 示例

我们 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 代码。

我们再次可以使用标准的 setuptools 模块,但这次我们需要从 Cython.Build 中获取一些额外的部分

from setuptools import setup, Extension
from Cython.Build import cythonize
extensions = [Extension("cos_module", sources=["cos_module.pyx"])]
setup(ext_modules=cythonize(extensions))

编译它

$ 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/scientific-python-lectures/advanced/interfacing_with_c/cython/cos_module.so
$ ls
build/ cos_module.c cos_module.pyx cos_module.so* setup.py

然后运行它

In [22]: import cos_module
In [23]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.so'>
File: /home/esc/git-working/scientific-python-lectures/advanced/interfacing_with_c/cython/cos_module.so
Docstring: <no docstring>
In [24]: dir(cos_module)
Out[24]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'__test__',
'cos_func']
In [25]: cos_module.cos_func(1.0)
Out[25]: 0.5403023058681398
In [26]: cos_module.cos_func(0.0)
Out[26]: 1.0
In [27]: cos_module.cos_func(3.14159265359)
Out[27]: -1.0

然后,稍微测试一下其健壮性,我们会看到我们得到了良好的错误消息

In [28]: 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/scientific-python-lectures/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 支持

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

可以使用 setuptools 编译它

from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy
extensions = [
Extension(
"cos_doubles",
sources=["_cos_doubles.pyx", "cos_doubles.c"],
include_dirs=[numpy.get_include()],
)
]
setup(ext_modules=cythonize(extensions))
  • 与之前的编译后的 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/scientific-python-lectures/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 matplotlib.pyplot as plt
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
plt.plot(x, y)
plt.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. 对每个示例进行微不足道的更改,并确保它可以正常工作。(例如,将 cos 更改为 sin。)

  3. 大多数示例,尤其是涉及 NumPy 的示例,可能仍然很脆弱,对输入错误的反应很差。尝试找出如何使这些示例崩溃,找出问题所在,并想出一个潜在的解决方案。以下是一些想法

    1. 数值溢出。

    2. 长度不同的输入和输出数组。

    3. 多维数组。

    4. 空数组

    5. 具有非 double 类型的数组

  4. 使用 %timeit IPython 魔法来测量各种解决方案的执行时间

2.8.8.1. Python-C-API

  1. 修改 NumPy 示例,使其接受两个输入参数,其中第二个参数是预分配的输出数组,使其类似于其他 NumPy 示例。

  2. 修改示例,使其只接受一个输入数组,并在原地修改该数组。

  3. 尝试修复示例以使用新的 NumPy 迭代器协议。如果您成功获得可行的解决方案,请在 github 上提交拉取请求。

  4. 您可能已经注意到,NumPy-C-API 示例是唯一一个不包装 cos_doubles 而是直接将 cos 函数应用于 NumPy 数组元素的 NumPy 示例。这与其他技术相比有什么优势吗?

  5. 您可以仅使用 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 示例。