探索Python与C代码交互的多种方式
在Python编程中,有时需要与C代码进行交互,以利用C语言的高性能或访问现有的C库。本文将介绍多种实现Python与C代码交互的方法,包括使用 ctypes 、编写C扩展模块、使用 Swig 等,同时会详细讲解每种方法的使用场景、实现步骤和注意事项。
1. 基础C代码示例
首先,我们来看一段基础的C代码示例,它包含了几个不同功能的函数和一个结构体:
/* sample.c */
#include <math.h>
/* 计算最大公约数 */
int gcd(int x, int y) {
int g = y;
while (x > 0) {
g = x;
x = y % x;
y = g;
}
return g;
}
/* 检查 (x0,y0) 是否属于曼德勃罗集 */
int in_mandel(double x0, double y0, int n) {
double x=0,y=0,xtemp;
while (n > 0) {
xtemp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtemp;
n -= 1;
if (x*x + y*y > 4) return 0;
}
return 1;
}
/* 两数相除 */
int divide(int a, int b, int *remainder) {
int quot = a / b;
*remainder = a % b;
return quot;
}
/* 计算数组元素的平均值 */
double avg(double *a, int n) {
int i;
double total = 0.0;
for (i = 0; i < n; i++) {
total += a[i];
}
return total / n;
}
/* C结构体 */
typedef struct Point {
double x,y;
} Point;
/* 使用C结构体的函数 */
double distance(Point *p1, Point *p2) {
return hypot(p1->x - p2->x, p1->y - p2->y);
}
这个代码包含了几个不同的功能:
- gcd 函数用于计算两个整数的最大公约数。
- in_mandel 函数用于检查一个点是否属于曼德勃罗集。
- divide 函数用于两数相除,并返回商和余数。
- avg 函数用于计算数组元素的平均值。
- Point 结构体表示二维平面上的一个点, distance 函数用于计算两个点之间的距离。
在后续的示例中,我们假设上述代码保存在 sample.c 文件中,其定义在 sample.h 文件中,并且已经编译成了 libsample 库。
2. 使用 ctypes 访问C代码
2.1 问题描述
当你有少量已编译成共享库或DLL的C函数,并且希望直接从Python调用这些函数,而无需编写额外的C代码或使用第三方扩展工具时,可以使用 ctypes 模块。
2.2 解决方案
ctypes 是Python标准库的一部分,使用它可以方便地访问C代码。以下是一个示例代码,展示了如何使用 ctypes 调用前面定义的C函数:
# sample.py
import ctypes
import os
# 尝试在当前目录中找到.so文件
_file = 'libsample.so'
_path = os.path.join(*(os.path.split(__file__)[:-1] + (_file,)))
_mod = ctypes.cdll.LoadLibrary(_path)
# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
gcd.restype = ctypes.c_int
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x, y, rem)
return quot, rem.value
# void avg(double *, int n)
# 定义一个特殊类型用于处理 'double *' 参数
class DoubleArrayType:
def from_param(self, param):
typename = type(param).__name__
if hasattr(self, 'from_' + typename):
return getattr(self, 'from_' + typename)(param)
elif isinstance(param, ctypes.Array):
return param
else:
raise TypeError("Can't convert %s" % typename)
# 从array.array对象转换
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
# 从列表/元组转换
def from_list(self, param):
val = ((ctypes.c_double)*len(param))(*param)
return val
from_tuple = from_list
# 从numpy数组转换
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
DoubleArray = DoubleArrayType()
_avg = _mod.avg
_avg.argtypes = (DoubleArray, ctypes.c_int)
_avg.restype = ctypes.c_double
def avg(values):
return _avg(values, len(values))
# struct Point { }
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double
如果一切顺利,你可以加载这个模块并使用其中的C函数:
>>> import sample
>>> sample.gcd(35,42)
7
>>> sample.in_mandel(0,0,500)
1
>>> sample.in_mandel(2.0,1.0,500)
0
>>> sample.divide(42,8)
(5, 2)
>>> sample.avg([1,2,3])
2.0
>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> sample.distance(p1,p2)
4.242640687119285
2.3 讨论
使用 ctypes 时,有几个方面需要注意:
- 库的位置 :确保共享库位于 sample.py 能够找到的位置。可以将生成的 .so 文件放在与Python支持代码相同的目录中。如果库安装在其他位置,需要相应地调整路径。可以使用 ctypes.util.find_library() 函数来查找标准库。
- 类型签名 :为每个C函数指定正确的参数类型和返回类型非常重要。 argtypes 属性用于指定函数的输入参数类型, restype 属性用于指定函数的返回类型。
- 指针参数 :对于使用指针的参数,通常需要构造一个兼容的 ctypes 对象并传递它。例如,在 divide 函数中,需要创建一个 ctypes.c_int 对象来传递余数。
- 数组处理 :C代码通常期望接收指针和长度来表示数组。为了支持多种Python数组类型,可以定义一个自定义类来处理不同类型的数组转换。
ctypes 是一个有用的库,但对于大型库,可能需要花费大量时间来指定所有的类型签名,并且需要编写许多小的包装函数和支持类。此外,如果对C接口的底层细节(如内存管理和错误处理)了解不足,很容易导致Python崩溃。作为 ctypes 的替代方案,可以考虑使用 CFFI ,它提供了类似的功能,但使用C语法并支持更高级的C代码类型。
3. 编写简单的C扩展模块
3.1 问题描述
如果你想直接使用Python扩展API编写一个简单的C扩展模块,而不使用其他工具,可以按照以下步骤进行。
3.2 解决方案
首先,确保你的C代码有正确的头文件,例如:
/* sample.h */
#include <math.h>
extern int gcd(int, int);
extern int in_mandel(double x0, double y0, int n);
extern int divide(int a, int b, int *remainder);
extern double avg(double *a, int n);
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
然后,编写一个扩展模块示例,展示编写扩展函数的基本原理:
#include "Python.h"
#include "sample.h"
/* int gcd(int, int) */
static PyObject *py_gcd(PyObject *self, PyObject *args) {
int x, y, result;
if (!PyArg_ParseTuple(args,"ii", &x, &y)) {
return NULL;
}
result = gcd(x,y);
return Py_BuildValue("i", result);
}
/* int in_mandel(double, double, int) */
static PyObject *py_in_mandel(PyObject *self, PyObject *args) {
double x0, y0;
int n;
int result;
if (!PyArg_ParseTuple(args, "ddi", &x0, &y0, &n)) {
return NULL;
}
result = in_mandel(x0,y0,n);
return Py_BuildValue("i", result);
}
/* int divide(int, int, int *) */
static PyObject *py_divide(PyObject *self, PyObject *args) {
int a, b, quotient, remainder;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
quotient = divide(a,b, &remainder);
return Py_BuildValue("(ii)", quotient, remainder);
}
/* 模块方法表 */
static PyMethodDef SampleMethods[] = {
{"gcd", py_gcd, METH_VARARGS, "Greatest common divisor"},
{"in_mandel", py_in_mandel, METH_VARARGS, "Mandelbrot test"},
{"divide", py_divide, METH_VARARGS, "Integer division"},
{ NULL, NULL, 0, NULL}
};
/* 模块结构体 */
static struct PyModuleDef samplemodule = {
PyModuleDef_HEAD_INIT,
"sample", /* 模块名称 */
"A sample module", /* 文档字符串 (可以为NULL) */
-1, /* 每个解释器的状态大小或 -1 */
SampleMethods /* 方法表 */
};
/* 模块初始化函数 */
PyMODINIT_FUNC
PyInit_sample(void) {
return PyModule_Create(&samplemodule);
}
为了编译扩展模块,创建一个 setup.py 文件:
# setup.py
from distutils.core import setup, Extension
setup(name='sample',
ext_modules=[
Extension('sample',
['pysample.c'],
include_dirs = ['/some/dir'],
define_macros = [('FOO','1')],
undef_macros = ['BAR'],
library_dirs = ['/usr/local/lib'],
libraries = ['sample']
)
]
)
使用以下命令编译生成共享库:
bash % python3 setup.py build_ext --inplace
running build_ext
building 'sample' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
-I/usr/local/include/python3.3m -c pysample.c
-o build/temp.macosx-10.6-x86_64-3.3/pysample.o
gcc -bundle -undefined dynamic_lookup
build/temp.macosx-10.6-x86_64-3.3/pysample.o \
-L/usr/local/lib -lsample -o sample.so
bash %
编译完成后,你可以将其作为模块导入并使用:
>>> import sample
>>> sample.gcd(35, 42)
7
>>> sample.in_mandel(0, 0, 500)
1
>>> sample.in_mandel(2.0, 1.0, 500)
0
>>> sample.divide(42, 8)
(5, 2)
3.3 讨论
在编写C扩展模块时,有几个重要的部分需要注意:
- 函数原型 :扩展函数通常使用以下原型编写:
static PyObject *py_func(PyObject *self, PyObject *args) {
...
}
PyObject 是C数据类型,表示Python对象。扩展函数接收一个Python对象元组 args ,并返回一个新的Python对象作为结果。
- 参数解析 : PyArg_ParseTuple() 函数用于将Python值转换为C表示。它接收一个格式字符串,指定所需的值类型,以及C变量的地址,用于存储转换后的结果。如果参数与格式字符串不匹配,会引发异常并返回 NULL 。
- 结果创建 : Py_BuildValue() 函数用于从C数据类型创建Python对象。它也接收一个格式代码,用于指定所需的类型。在扩展函数中,它用于将结果返回给Python。
- 方法表 :每个扩展模块都需要定义一个方法表,列出C函数、Python中使用的名称以及文档字符串。这个表在模块初始化时使用。
- 模块初始化 : PyInit_sample() 函数是模块的初始化函数,在模块首次导入时执行。它的主要工作是在解释器中注册模块对象。
需要注意的是,关于用C函数扩展Python的内容还有很多,C API包含500多个函数。这个示例只是学习这个主题的第一步,建议从 PyArg_ParseTuple() 和 Py_BuildValue() 函数的文档开始学习,并逐步扩展。
4. 编写处理数组的扩展函数
4.1 问题描述
如果你想编写一个C扩展函数,处理由 array 模块或 NumPy 等库创建的连续数据数组,并且希望该函数具有广泛的适用性,而不是特定于某个数组库,可以使用缓冲区协议。
4.2 解决方案
以下是一个示例,展示了如何编写一个C扩展函数,接收一个数据数组并调用前面定义的 avg 函数:
/* 调用 double avg(double *, int) */
static PyObject *py_avg(PyObject *self, PyObject *args) {
PyObject *bufobj;
Py_buffer view;
double result;
/* 获取传递的Python对象 */
if (!PyArg_ParseTuple(args, "O", &bufobj)) {
return NULL;
}
/* 尝试从缓冲区提取信息 */
if (PyObject_GetBuffer(bufobj, &view,
PyBUF_ANY_CONTIGUOUS | PyBUF_FORMAT) == -1) {
return NULL;
}
if (view.ndim != 1) {
PyErr_SetString(PyExc_TypeError, "Expected a 1-dimensional array");
PyBuffer_Release(&view);
return NULL;
}
/* 检查数组元素类型 */
if (strcmp(view.format,"d") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected an array of doubles");
PyBuffer_Release(&view);
return NULL;
}
/* 将原始缓冲区和大小传递给C函数 */
result = avg(view.buf, view.shape[0]);
/* 释放缓冲区 */
PyBuffer_Release(&view);
return Py_BuildValue("d", result);
}
以下是这个扩展函数的使用示例:
>>> import array
>>> avg(array.array('d',[1,2,3]))
2.0
>>> import numpy
>>> avg(numpy.array([1.0,2.0,3.0]))
2.0
>>> avg([1,2,3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' does not support the buffer interface
>>> avg(b'Hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Expected an array of doubles
>>> a = numpy.array([[1.,2.,3.],[4.,5.,6.]])
>>> avg(a[:,2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: ndarray is not contiguous
>>> sample.avg(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Expected a 1-dimensional array
>>> sample.avg(a[0])
2.0
4.3 讨论
传递数组对象给C函数是扩展函数最常见的应用之一。许多Python应用(从图像处理程序到科学计算)都基于高性能的数组处理。通过编写能够接收和处理数组的代码,可以开发出与这些应用程序良好配合的程序。
主要的关键点是 PyBuffer_GetBuffer() 函数。当传递任意Python对象时,它会尝试获取底层内存表示的信息。如果无法获取,会引发异常并返回 -1。传递给 PyBuffer_GetBuffer() 的特殊标志可以提供有关所需内存缓冲区类型的额外提示。例如, PyBUF_ANY_CONTIGUOUS 指定请求的是连续的内存区域。
对于数组、字节字符串和其他类似对象, Py_buffer 结构会填充有关底层内存的信息,包括内存指针、大小、元素大小、格式等。在这个示例中,我们确保获取的是连续的双精度浮点数数组。通过检查 format 属性,可以验证元素类型是否为双精度浮点数。
在处理完缓冲区后,必须使用 PyBuffer_Release() 函数释放底层缓冲区,以正确管理对象的引用计数。
需要注意的是,这个示例只是处理数组的一小部分代码。在处理数组时,可能会遇到与多维数据、数据步长、不同数据类型等相关的问题,需要进一步学习。如果需要编写大量使用数组处理的扩展,可以考虑使用 Cython 。
5. 管理C扩展模块中的不透明指针
5.1 问题描述
如果你有一个扩展模块,需要处理指向C数据结构的指针,但不想向Python代码暴露该结构的内部实现,可以使用封装对象来管理不透明指针。
5.2 解决方案
考虑以下C代码片段:
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
以下是一个扩展代码示例,使用封装对象来封装 Point 结构和 distance 函数:
/* 点的析构函数 */
static void del_Point(PyObject *obj) {
free(PyCapsule_GetPointer(obj,"Point"));
}
/* 辅助函数 */
static Point *PyPoint_AsPoint(PyObject *obj) {
return (Point *) PyCapsule_GetPointer(obj, "Point");
}
static PyObject *PyPoint_FromPoint(Point *p, int must_free) {
return PyCapsule_New(p, "Point", must_free ? del_Point : NULL);
}
/* 创建新的Point对象 */
static PyObject *py_Point(PyObject *self, PyObject *args) {
Point *p;
double x,y;
if (!PyArg_ParseTuple(args,"dd",&x,&y)) {
return NULL;
}
p = (Point *) malloc(sizeof(Point));
p->x = x;
p->y = y;
return PyPoint_FromPoint(p, 1);
}
static PyObject *py_distance(PyObject *self, PyObject *args) {
Point *p1, *p2;
PyObject *py_p1, *py_p2;
double result;
if (!PyArg_ParseTuple(args,"OO",&py_p1, &py_p2)) {
return NULL;
}
if (!(p1 = PyPoint_AsPoint(py_p1))) {
return NULL;
}
if (!(p2 = PyPoint_AsPoint(py_p2))) {
return NULL;
}
result = distance(p1,p2);
return Py_BuildValue("d", result);
}
从Python中使用这些函数的示例如下:
>>> import sample
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<capsule object "Point" at 0x1004ea330>
>>> p2
<capsule object "Point" at 0x1005d1db0>
>>> sample.distance(p1,p2)
2.8284271247461903
5.3 讨论
封装对象类似于类型化的C指针。内部包含一个通用指针和一个标识符名称,可以使用 PyCapsule_New() 函数轻松创建。还可以将可选的析构函数附加到封装对象上,以便在封装对象被垃圾回收时释放内存。
要提取封装对象中包含的指针,可以使用 PyCapsule_GetPointer() 函数并指定名称。如果提供的名称与封装对象中的名称不匹配(或发生其他错误),会引发异常并返回 NULL 。
在这个示例中,编写了两个辅助函数 PyPoint_FromPoint() 和 PyPoint_AsPoint() ,用于处理从封装对象创建和关闭 Point 实例的机制。在任何扩展函数中,我们都会使用这些函数,而不是直接操作封装对象。这样设计是为了方便未来对 Point 对象封装进行修改。例如,如果决定使用其他方式代替封装对象,只需要修改这两个函数。
使用封装对象的一个微妙之处是垃圾回收和内存管理。 PyPoint_FromPoint() 函数接受一个 must_free 参数,指示当封装对象被销毁时,底层的 Point * 结构是否应该被回收。在处理某些类型的C代码时,可能很难处理相关的问题。通过这个额外的参数,可以将控制权交还给程序员。此外,还可以使用 PyCapsule_SetDestructor() 函数更改与现有封装对象关联的析构函数。
封装对象是为使用结构的某些类型的C代码创建接口的合理解决方案。例如,有时不需要暴露结构的内部实现,或者将其转换为完整的扩展类型。使用封装对象,可以轻松地将其封装在轻量级包装器中,并传递给其他扩展函数。
6. 从C调用Python
6.1 问题描述
如果你想安全地执行一个Python可调用对象,并将结果返回给C,可以按照以下步骤进行。例如,你可能在编写C代码时,希望使用Python函数作为回调函数。
6.2 解决方案
以下是一个C示例,展示了如何安全地从C调用Python函数:
#include <Python.h>
/* 在Python解释器中执行 func(x,y)。函数的参数和返回结果必须是浮点数 (Python floats)。 */
double call_func(PyObject *func, double x, double y) {
PyObject *args;
PyObject *kwargs;
PyObject *result = 0;
double retval;
/* 确保我们拥有GIL */
PyGILState_STATE state = PyGILState_Ensure();
/* 检查 func 是否为合适的可调用对象 */
if (!PyCallable_Check(func)) {
fprintf(stderr,"call_func: expected a callable\n");
goto fail;
}
/* 构建参数 */
args = Py_BuildValue("(dd)", x, y);
kwargs = NULL;
/* 调用函数 */
result = PyObject_Call(func, args, kwargs);
Py_DECREF(args);
Py_XDECREF(kwargs);
/* 检查Python异常 (如果有) */
if (PyErr_Occurred()) {
PyErr_Print();
goto fail;
}
/* 检查结果是否为浮点数对象 */
if (!PyFloat_Check(result)) {
fprintf(stderr,"call_func: callable didn't return a float\n");
goto fail;
}
/* 创建返回值 */
retval = PyFloat_AsDouble(result);
Py_DECREF(result);
/* 恢复之前的GIL状态并返回值 */
PyGILState_Release(state);
return retval;
fail:
Py_XDECREF(result);
PyGILState_Release(state);
abort(); // 改为更合适的处理方式
}
要使用这个函数,你需要获取一个现有的Python可调用对象的引用,并将其传递给它。以下是一个简单的示例,展示了如何从内置的Python解释器中调用函数:
#include <Python.h>
/* call_func() 的定义与上面相同 */
...
/* 从模块中加载符号 */
PyObject *import_name(const char *modname, const char *symbol) {
PyObject *u_name, *module;
u_name = PyUnicode_FromString(modname);
module = PyImport_Import(u_name);
Py_DECREF(u_name);
return PyObject_GetAttrString(module, symbol);
}
/* 简单的嵌入示例 */
int main() {
PyObject *pow_func;
double x;
Py_Initialize();
/* 获取 math.pow 函数的引用 */
pow_func = import_name("math","pow");
/* 使用我们的 call_func() 代码调用它 */
for (x = 0.0; x < 10.0; x += 0.1) {
printf("%0.2f %0.2f\n", x, call_func(pow_func,x,2.0));
}
/* 完成 */
Py_DECREF(pow_func);
Py_Finalize();
return 0;
}
要编译这个示例,需要编译C代码并将其与Python解释器链接。以下是一个 makefile 示例:
all::
cc -g embed.c -I/usr/local/include/python3.3m \
-L/usr/local/lib/python3.3/config-3.3m -lpython3.3m
编译并运行生成的可执行文件将产生类似以下的输出:
0.00 0.00
0.10 0.01
0.20 0.04
0.30 0.09
0.40 0.16
...
以下是一个稍微不同的示例,展示了一个扩展函数,它接收一个可调用对象和参数,然后将它们传递给 call_func() 进行测试:
/* 用于检查 C-Python 回调函数的扩展函数 */
PyObject *py_call_func(PyObject *self, PyObject *args) {
PyObject *func;
double x, y, result;
if (!PyArg_ParseTuple(args,"Odd", &func,&x,&y)) {
return NULL;
}
result = call_func(func, x, y);
return Py_BuildValue("d", result);
}
可以使用以下方式测试这个扩展函数:
>>> import sample
>>> def add(x,y):
... return x+y
...
>>> sample.call_func(add,3,4)
7.0
6.3 讨论
从C调用Python时,最重要的是记住C通常应该始终是“主导者”。C负责创建参数、调用Python函数、检查异常、检查类型、提取返回值等。
首先,必须有一个表示要调用的可调用对象的Python对象。这可以是函数、类、方法、内置方法或任何实现了 __call__() 操作的对象。可以使用 PyCallable_Check() 函数来确保对象是可调用的。
在C代码中处理错误需要仔细考虑。一般来说,不能直接引发Python异常。相反,错误应该以对C代码有意义的方式处理。在解决方案中,使用 goto 语句将控制权传递给错误处理块,调用 abort() 函数终止整个程序。在实际代码中,可能需要更谨慎地处理错误,例如返回状态码。
调用函数相对简单,使用 PyObject_Call() 函数,提供可调用对象、参数元组和可选的命名参数字典。可以使用 Py_BuildValue() 函数创建参数元组或字典。如果没有命名参数,可以传递 NULL 。调用函数后,需要使用 Py_DECREF() 或 Py_XDECREF() 函数清理参数。
调用Python函数后,需要检查是否发生了异常。可以使用 PyErr_Occurred() 函数进行检查。但确定如何响应异常比较困难,因为在C中没有像Python那样的异常处理系统。可以设置错误状态码、记录错误或执行其他有意义的处理。
从Python函数返回的值中获取信息通常需要进行一些类型检查和值提取。可以使用Python具体对象层的函数来完成。在解决方案中,使用 PyFloat_Check() 和 PyFloat_AsDouble() 函数检查和提取浮点数类型的值。
最后,从C调用Python的一个复杂部分是管理Python解释器的全局解释器锁(GIL)。每次从C访问Python时,必须确保正确获取和释放GIL。使用 PyGILState_Ensure() 和 PyGILState_Release() 函数可以确保操作正确。 PyGILState_Ensure() 函数在执行完成前始终保证调用线程对Python解释器具有独占访问权。在这个点上,C代码可以自由使用任何Python C API函数。 PyGILState_Release() 函数用于恢复解释器的初始状态。
需要注意的是,每个 PyGILState_Ensure() 调用后面都必须跟一个相应的 PyGILState_Release() 调用,即使发生了错误。在解决方案中, goto 语句看起来可能是糟糕的设计,但它用于将控制权传递给执行所需步骤的常规退出块。可以将 fail: 后面的代码看作Python中 finally: 块的类似物。
7. 在C扩展中释放GIL
7.1 问题描述
如果你有一个C扩展代码,希望与Python解释器中的其他线程并发执行,需要释放和重新获取全局解释器锁(GIL)。
7.2 解决方案
在C扩展代码中,可以通过在代码中插入以下宏来释放和重新获取GIL:
#include "Python.h"
...
PyObject *pyfunc(PyObject *self, PyObject *args) {
...
Py_BEGIN_ALLOW_THREADS
// 多线程C代码。不能使用Python API函数
...
Py_END_ALLOW_THREADS
...
return result;
}
7.3 讨论
只有在能够保证C代码中不会执行Python C API函数时,才能安全地释放GIL。常见的释放GIL的例子包括对C数组执行复杂计算的代码(例如,在 numpy 类型的扩展中),或执行阻塞I/O操作的代码(例如,读写文件描述符)。
当GIL被释放时,其他Python线程可以在解释器中执行。 Py_END_ALLOW_THREADS 宏会阻塞执行,直到调用线程重新获得解释器中的GIL。
8. 合并C和Python线程
8.1 问题描述
如果你有一个程序,包含C、Python和线程,其中一些线程是从C创建的,超出了Python解释器的控制范围,并且一些线程使用Python C API,需要正确初始化和管理Python解释器的全局解释器锁(GIL)。
8.2 解决方案
如果要合并C、Python和线程,需要确保正确初始化和管理Python解释器的全局解释器锁(GIL)。在C程序中包含以下代码,并确保在创建任何线程之前调用它:
#include <Python.h>
...
if (!PyEval_ThreadsInitialized()) {
PyEval_InitThreads();
}
...
对于任何使用Python对象或Python C API的C代码,首先要确保正确获取和释放GIL。可以使用 PyGILState_Ensure() 和 PyGILState_Release() 函数,如下所示:
...
/* 确保我们拥有GIL */
PyGILState_STATE state = PyGILState_Ensure();
/* 使用解释器中的函数 */
...
/* 恢复之前的GIL状态并返回值 */
PyGILState_Release(state);
...
每个 PyGILState_Ensure() 调用后面都必须跟一个相应的 PyGILState_Release() 调用。
8.3 讨论
在基于C和Python的高级应用程序中,同时发生多个事件并不罕见。这通常意味着C代码、Python代码、C线程和Python线程的混合。只要仔细检查解释器的初始化正确性和从C代码调用管理GIL的正确性,一切应该可以正常工作。
需要注意的是, PyGILState_Ensure() 调用不会立即获取解释器或中断它。如果另一段代码正在执行,该函数会阻塞,直到该代码决定释放GIL。在解释器内部,会定期切换线程,所以即使另一个线程正在执行,调用线程迟早会启动,尽管可能需要先等待一段时间。
9. 使用Swig包装C代码
9.1 问题描述
如果你有现有的C代码,希望将其作为C扩展模块访问,可以使用Swig代码生成器来自动创建包装代码。
9.2 解决方案
Swig通过解析C头文件并自动生成扩展代码来工作。首先,需要有一个C头文件,例如:
/* sample.h */
#include <math.h>
extern int gcd(int, int);
extern int in_mandel(double x0, double y0, int n);
extern int divide(int a, int b, int *remainder);
extern double avg(double *a, int n);
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
如果有头文件,下一步是编写一个Swig“接口”文件,扩展名为 .i ,示例如下:
// sample.i - Swig interface
%module sample
%{
#include "sample.h"
%}
/* 自定义 */
%extend Point {
/* Point对象的构造函数 */
Point(double x, double y) {
Point *p = (Point *) malloc(sizeof(Point));
p->x = x;
p->y = y;
return p;
};
};
/* 将 int *remainder 映射为输出参数 */
%include typemaps.i
%apply int *OUTPUT { int * remainder };
/* 将参数模板 (double *a, int n) 映射到数组 */
%typemap(in) (double *a, int n)(Py_buffer view) {
view.obj = NULL;
if (PyObject_GetBuffer($input, &view, PyBUF_ANY_CONTIGUOUS | PyBUF_FORMAT) == -1) {
SWIG_fail;
}
if (strcmp(view.format,"d") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected an array of doubles");
SWIG_fail;
}
$1 = (double *) view.buf;
$2 = view.len / sizeof(double);
}
%typemap(freearg) (double *a, int n) {
if (view$argnum.obj) {
PyBuffer_Release(&view$argnum);
}
}
/* 包含在扩展模块中的C声明 */
extern int gcd(int, int);
extern int in_mandel(double x0, double y0, int n);
extern int divide(int a, int b, int *remainder);
extern double avg(double *a, int n);
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
编写完接口文件后,使用Swig作为命令行工具调用:
bash % swig -python -py3 sample.i
bash %
Swig命令的输出将生成两个文件: sample_wrap.c 和 sample.py 。 sample.py 是用户导入的文件, sample_wrap.c 是需要编译成名为 _sample 的支持模块的C代码。可以使用与普通扩展模块相同的方法进行编译。例如,创建一个 setup.py 文件:
# setup.py
from distutils.core import setup, Extension
setup(name='sample',
py_modules=['sample.py'],
ext_modules=[
Extension('_sample',
['sample_wrap.c'],
include_dirs = [],
define_macros = [],
undef_macros = [],
library_dirs = [],
libraries = ['sample']
)
]
)
使用以下命令编译并验证:
bash % python3 setup.py build_ext --inplace
running build_ext
building '_sample' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
-I/usr/local/include/python3.3m -c sample_wrap.c
-o build/temp.macosx-10.6-x86_64-3.3/sample_wrap.o
sample_wrap.c: In function 'SWIG_InitializeModule':
sample_wrap.c:3589: warning: statement with no effect
gcc -bundle -undefined dynamic_lookup build/temp.macosx-10.6-x86_64-3.3/sample.o
build/temp.macosx-10.6-x86_64-3.3/sample_wrap.o -o _sample.so -lsample
bash %
如果一切顺利,你可以直接使用生成的C扩展模块:
>>> import sample
>>> sample.gcd(42,8)
2
>>> sample.divide(42,8)
[5, 2]
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> sample.distance(p1,p2)
2.8284271247461903
>>> p1.x
2.0
>>> p1.y
3.0
>>> import array
>>> a = array.array('d',[1,2,3])
>>> sample.avg(a)
2.0
9.3 讨论
Swig是最古老的创建扩展模块的工具之一,其历史可以追溯到Python 1.4。当前版本支持Python 3。主要使用Swig的是那些有大量C代码库,并希望使用Python作为高级控制语言来访问这些代码的程序员。例如,用户可能有包含数千个函数和各种数据结构的C代码,希望从Python中访问这些代码。Swig可以自动化大部分包装代码的生成过程。
所有Swig接口通常以简短的介绍开始:
%module sample
%{
#include "sample.h"
%}
这只是声明扩展模块的名称,并定义需要包含的C头文件,以确保所有代码能够编译。在 %{ %} 之间的代码会直接插入到生成的代码中,因此需要包含所有必要的头文件和其他定义。
在Swig接口的末尾,列出了需要包含在扩展中的C声明。通常直接从头文件复制这些声明。在示例中,我们直接插入了头文件:
%module sample
%{
#include "sample.h"
%}
...
extern int gcd(int, int);
extern int in_mandel(double x0, double y0, int n);
extern int divide(int a, int b, int *remainder);
extern double avg(double *a, int n);
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
需要注意的是,这些声明告诉Swig你希望包含在Python模块中的内容。通常会编辑这个声明列表,以进行必要的更改。例如,如果不想包含某些声明,可以从列表中删除它们。
使用Swig的一个复杂之处是它可以对C代码应用各种自定义。这是一个很大的主题,无法详细讨论,但在示例中展示了一些自定义。
第一个自定义使用 %extend 指令,允许将方法附加到现有的结构和类定义上。在示例中,用于为 Point 结构添加构造函数方法。这样可以更方便地使用 Point 结构:
>>> p1 = sample.Point(2,3)
如果省略这个自定义,创建 Point 对象会更加麻烦:
>>> # 如果省略 %extend Point
>>> p1 = sample.Point()
>>> p1.x = 2.0
>>> p1.y = 3
第二个自定义包括包含 typemaps.i 库和 %apply 指令,告诉Swig将 int *remainder 参数签名视为输出值。实际上,这是一种模式匹配规则。在后续的所有声明中,都会应用这个规则。
综上所述,通过多种方法可以实现Python与C代码的交互,每种方法都有其适用场景和优缺点。在实际应用中,需要根据具体需求选择合适的方法。同时,要注意各种方法中的细节,如内存管理、错误处理、GIL管理等,以确保代码的稳定性和性能。希望本文的介绍能够帮助你更好地理解和应用这些技术。
探索Python与C代码交互的多种方式
10. 技术点总结与对比
为了更清晰地了解各种Python与C代码交互方法的特点,下面通过表格进行总结对比:
| 交互方法 | 适用场景 | 优点 | 缺点 |
| — | — | — | — |
| ctypes | 少量已编译C函数,无需额外C代码或第三方工具 | 简单易用,是Python标准库一部分 | 处理大型库时需大量类型签名和包装函数,对C底层细节要求高 |
| 编写简单C扩展模块 | 直接使用Python扩展API编写简单模块 | 灵活性高,可直接控制扩展功能 | 学习曲线较陡,C API函数众多 |
| 编写处理数组的扩展函数 | 处理连续数据数组,希望函数有广泛适用性 | 能支持多种数组库 | 处理复杂数组问题需进一步学习 |
| 管理C扩展模块中的不透明指针 | 处理指向C数据结构的指针,不暴露结构内部实现 | 封装性好,方便管理指针 | 垃圾回收和内存管理需谨慎处理 |
| 从C调用Python | 在C代码中使用Python函数作为回调函数 | 实现C与Python的双向交互 | C代码中错误处理和GIL管理复杂 |
| 在C扩展中释放GIL | 让C扩展代码与其他Python线程并发执行 | 提高并发性能 | 需确保C代码不执行Python C API函数 |
| 合并C和Python线程 | 程序包含C、Python和线程,部分线程使用Python C API | 实现多线程环境下的交互 | 需正确初始化和管理GIL |
| 使用Swig包装C代码 | 有大量C代码库,希望用Python访问 | 自动化生成包装代码,历史悠久 | 自定义规则复杂,需学习Swig语法 |
11. 实际应用案例分析
11.1 图像处理应用
在图像处理应用中,经常需要对大量的图像数据进行高性能处理。可以使用编写处理数组的扩展函数方法,结合 NumPy 库来实现。例如,对于图像的均值滤波操作,可以编写一个C扩展函数,接收 NumPy 数组表示的图像数据,然后在C代码中进行高效的计算。
/* 调用 double avg(double *, int) 进行图像均值滤波 */
static PyObject *py_image_avg_filter(PyObject *self, PyObject *args) {
PyObject *bufobj;
Py_buffer view;
double result;
/* 获取传递的Python对象 */
if (!PyArg_ParseTuple(args, "O", &bufobj)) {
return NULL;
}
/* 尝试从缓冲区提取信息 */
if (PyObject_GetBuffer(bufobj, &view,
PyBUF_ANY_CONTIGUOUS | PyBUF_FORMAT) == -1) {
return NULL;
}
if (view.ndim != 2) {
PyErr_SetString(PyExc_TypeError, "Expected a 2-dimensional array");
PyBuffer_Release(&view);
return NULL;
}
/* 检查数组元素类型 */
if (strcmp(view.format,"d") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected an array of doubles");
PyBuffer_Release(&view);
return NULL;
}
/* 进行均值滤波计算,这里简单示例为计算整个图像的均值 */
result = avg(view.buf, view.shape[0] * view.shape[1]);
/* 释放缓冲区 */
PyBuffer_Release(&view);
return Py_BuildValue("d", result);
}
在Python中调用这个扩展函数:
import numpy as np
import sample
image = np.random.rand(100, 100)
result = sample.image_avg_filter(image)
print(result)
11.2 科学计算应用
在科学计算中,可能会有一些复杂的算法已经用C语言实现,希望在Python中调用。可以使用 ctypes 或 Swig 方法。例如,对于一个求解线性方程组的C函数,使用 ctypes 调用:
import ctypes
import os
# 尝试在当前目录中找到.so文件
_file = 'liblinear_solver.so'
_path = os.path.join(*(os.path.split(__file__)[:-1] + (_file,)))
_mod = ctypes.cdll.LoadLibrary(_path)
# 定义函数参数和返回类型
solve_linear_eq = _mod.solve_linear_eq
solve_linear_eq.argtypes = (ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.c_int)
solve_linear_eq.restype = ctypes.c_int
# 准备数据
A = [1.0, 2.0, 3.0, 4.0]
b = [5.0, 6.0]
n = 2
A_array = (ctypes.c_double * len(A))(*A)
b_array = (ctypes.c_double * len(b))(*b)
# 调用函数
result = solve_linear_eq(A_array, b_array, n)
print(result)
12. 总结与展望
通过本文的介绍,我们了解了多种Python与C代码交互的方法,每种方法都有其独特的优势和适用场景。在实际开发中,需要根据具体需求和项目特点选择合适的方法。
对于初学者来说, ctypes 是一个不错的入门选择,它简单易用,可以快速实现Python与C代码的交互。而对于有一定C编程基础,希望深入控制扩展功能的开发者,编写简单C扩展模块或使用 Swig 可能更合适。
未来,随着Python和C语言的不断发展,Python与C代码的交互方式可能会更加多样化和便捷。例如, CFFI 作为 ctypes 的替代方案,可能会在未来得到更广泛的应用。同时,随着多线程和并行计算的需求增加,如何更好地管理GIL和实现多线程环境下的高效交互也将是一个重要的研究方向。
总之,掌握Python与C代码的交互技术,可以充分发挥两种语言的优势,提高程序的性能和开发效率。希望本文能够为读者在这方面的学习和实践提供有价值的参考。
13. 流程图展示
下面是使用 ctypes 调用C函数的流程图:
graph TD;
A[开始] --> B[加载共享库];
B --> C[定义函数参数和返回类型];
C --> D[准备数据];
D --> E[调用C函数];
E --> F[处理返回结果];
F --> G[结束];
14. 注意事项列表
在进行Python与C代码交互时,还需要注意以下事项:
- 内存管理 :无论是使用哪种交互方法,都要特别注意内存的分配和释放,避免出现内存泄漏问题。例如,在使用不透明指针管理时,要正确处理析构函数。
- 错误处理 :C代码中的错误处理与Python不同,需要以对C代码有意义的方式处理错误,不能直接引发Python异常。
- GIL管理 :在涉及多线程时,GIL的管理非常重要。确保在访问Python时正确获取和释放GIL,避免出现数据损坏或程序崩溃的情况。
- 兼容性 :要确保C代码编译的共享库与Python解释器的架构、字长、编译器等兼容。
通过遵循这些注意事项,可以提高Python与C代码交互的稳定性和可靠性。
超级会员免费看

被折叠的 条评论
为什么被折叠?



