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 数组作为参数传递给函数,以及如何使用(旧的)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
这应该得到以下图形
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.ctypes 和numpy.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
编译它,而必须使用 make
和 gcc
的组合
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()
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 允许对头文件中找到的函数进行更细粒度的包含/排除,请查看文档以了解详细信息。
生成编译后的包装器是一个两阶段过程
在接口文件上运行
swig
可执行文件以生成文件cos_module_wrap.c
,它是自动生成的 Python C 扩展的源文件,以及cos_module.py
,它是自动生成的纯 Python 模块。将
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()
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)
请注意额外的关键字,例如 cdef
和 extern
。此外,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()
2.8.6. 总结¶
在本节中,介绍了四种不同的与本机代码交互的技术。下表大致总结了这些技术的一些方面。
x |
CPython 的一部分 |
编译 |
自动生成 |
NumPy 支持 |
---|---|---|---|---|
Python-C-API |
|
|
|
|
Ctypes |
|
|
|
|
Swig |
|
|
|
|
Cython |
|
|
|
|
在所有三种技术中,Cython 是最现代和最先进的。特别是,通过将类型添加到 Python 代码来增量优化代码的能力是独一无二的。
2.8.7. 进一步阅读和参考¶
Gaël Varoquaux 的关于避免数据复制的博客文章提供了一些关于如何巧妙地处理内存管理的见解。如果您在处理大型数据集时遇到问题,请参考此文章以获取灵感。
2.8.8. 练习¶
由于这是一个全新的章节,练习被视为对下一步要关注内容的指引,因此请选择您觉得最有趣的练习。如果您有好的练习想法,请告诉我们!
下载每个示例的源代码,并在您的机器上编译并运行它们。
对每个示例进行微不足道的更改,并确保它可以正常工作。(例如,将
cos
更改为sin
。)大多数示例,尤其是涉及 NumPy 的示例,可能仍然很脆弱,对输入错误的反应很差。尝试找出如何使这些示例崩溃,找出问题所在,并想出一个潜在的解决方案。以下是一些想法
数值溢出。
长度不同的输入和输出数组。
多维数组。
空数组
具有非
double
类型的数组
使用
%timeit
IPython 魔法来测量各种解决方案的执行时间
2.8.8.1. Python-C-API¶
修改 NumPy 示例,使其接受两个输入参数,其中第二个参数是预分配的输出数组,使其类似于其他 NumPy 示例。
修改示例,使其只接受一个输入数组,并在原地修改该数组。
尝试修复示例以使用新的 NumPy 迭代器协议。如果您成功获得可行的解决方案,请在 github 上提交拉取请求。
您可能已经注意到,NumPy-C-API 示例是唯一一个不包装
cos_doubles
而是直接将cos
函数应用于 NumPy 数组元素的 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 类型映射包装它吗?如果不能,为什么?对于这种情况,是否有解决方法?(提示:您知道输出数组的大小,因此可能可以从返回的double *
构造一个 NumPy 数组。)
2.8.8.4. Cython¶
查看 Cython 自动生成的代码。仔细看看 Cython 插入的一些注释。您看到了什么?
查看 Cython 文档中的 使用 NumPy 部分,了解如何逐步优化使用 NumPy 的纯 Python 脚本。
修改 NumPy 示例,使
cos_doubles_func
为您处理预分配,从而使其更像 NumPy-C-API 示例。