Python高级技术之:`Python`的`C`扩展模块:从`C`代码到`Python`模块的编译和链接。

咳咳,各位观众老爷们,晚上好!今天咱们来聊点硬核的,关于Python的C扩展。别害怕,不是让你们重学C语言,只是教你们怎么把C语言写好的“零件”塞到Python这个“玩具箱”里。

开场白:Python与C的那些事儿

Python这玩意儿,上手快,用着爽,但有时候吧,速度有点捉急。尤其是在处理计算密集型的任务时,比如图像处理、科学计算,那速度简直让人想砸电脑。这时候,我们就需要C语言老大哥来救场了。C语言效率高啊,直接操作硬件,速度嗖嗖的。

所以,Python的C扩展就应运而生了。它允许我们用C语言编写一些高性能的模块,然后在Python代码里调用,这样既能享受Python的便利,又能拥有C语言的速度。是不是想想就激动?

第一幕:C代码的编写——零件的设计图纸

首先,我们要用C语言编写我们的“零件”。这个零件要符合Python的C扩展规范,简单来说,就是要提供一些特定的函数和结构体,让Python知道怎么调用它。

一个最简单的例子:计算两个数的和。

#include <Python.h>

// 我们的函数:计算两个数的和
static PyObject* add_numbers(PyObject* self, PyObject* args) {
    double a, b;

    // 从Python传递过来的参数中解析出两个double类型的数
    if (!PyArg_ParseTuple(args, "dd", &a, &b)) {
        return NULL; // 参数解析失败,返回NULL
    }

    // 计算和
    double sum = a + b;

    // 将结果转换为Python对象(这里是float)
    return PyFloat_FromDouble(sum);
}

// 方法列表:告诉Python我们的模块有哪些函数
static PyMethodDef AddMethods[] = {
    {"add",  add_numbers, METH_VARARGS, "Add two numbers."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

// 模块定义:告诉Python我们的模块叫什么名字
static struct PyModuleDef addmodule = {
    PyModuleDef_HEAD_INIT,
    "add",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    AddMethods
};

// 模块初始化函数:Python导入模块时会调用这个函数
PyMODINIT_FUNC
PyInit_add(void)
{
    return PyModule_Create(&addmodule);
}

这段代码乍一看有点长,但其实结构很清晰:

  1. #include <Python.h>: 必须包含的头文件,提供了Python C API。
  2. add_numbers 函数: 我们的核心逻辑,负责计算两个数的和。
    • PyObject* self: 类似于Python类中的self,但在这里通常不用管它。
    • PyObject* args: 从Python传递过来的参数,我们需要用PyArg_ParseTuple解析它们。
    • PyArg_ParseTuple(args, "dd", &a, &b): 这个函数很重要,它负责把Python传递过来的参数(args)解析成C语言可以使用的类型。"dd" 表示我们期望接收两个double类型的参数,&a&b 是用来存储解析出来的参数的地址。如果解析失败,返回NULL
    • PyFloat_FromDouble(sum): 把C语言的double类型的sum转换成Python的float对象,因为Python只能识别Python对象。
  3. AddMethods: 一个PyMethodDef类型的数组,用来告诉Python我们的模块有哪些函数可以调用。
    • {"add", add_numbers, METH_VARARGS, "Add two numbers."}: 定义了一个名为add的函数,它对应C语言中的add_numbers函数,METH_VARARGS表示这个函数接受可变数量的参数,最后一个字符串是函数的文档字符串。
    • {NULL, NULL, 0, NULL}: 必须以一个全NULL的结构体结尾,表示方法列表结束。
  4. addmodule: 一个PyModuleDef类型的结构体,用来定义我们的模块。
    • "add": 模块的名字,Python中通过import add来导入这个模块。
    • AddMethods: 指向我们定义的方法列表。
  5. PyInit_add: 模块的初始化函数,Python在导入模块时会调用这个函数。
    • PyModule_Create(&addmodule): 创建一个Python模块对象,并返回它。

第二幕:编译——把图纸变成零件

有了C代码,接下来就要把它编译成一个共享库,也就是Python可以加载的模块。这个过程稍微有点复杂,需要用到一些工具,比如gccdistutils

  1. 创建一个setup.py文件: 这个文件用来告诉distutils怎么编译我们的C代码。

    from distutils.core import setup, Extension
    
    module1 = Extension('add',  # 模块的名字,要和C代码里的模块名一致
                        sources = ['add.c'])  # C代码的文件名
    
    setup (name = 'add',  # 包的名字,可以和模块名不一样
           version = '1.0',
           description = 'This is a demo package',
           ext_modules = [module1])

    这个setup.py文件很简单:

    • Extension('add', sources = ['add.c']): 定义了一个扩展模块,名字是add,源代码是add.c
    • setup(...): 设置了一些包的信息,比如名字、版本、描述等等。ext_modules参数指定了我们要编译的扩展模块。
  2. 编译: 在命令行中运行以下命令:

    python setup.py build_ext --inplace

    这个命令会调用distutils来编译我们的C代码,生成一个名为add.so(或者add.pyd,取决于你的操作系统)的共享库文件。--inplace选项表示把编译好的共享库放在当前目录下。

    如果一切顺利,你会在当前目录下看到一个add.so(或者add.pyd)文件。这个就是我们的Python C扩展模块了!

第三幕:使用——把零件装进玩具箱

现在,我们可以像使用普通的Python模块一样使用我们的C扩展模块了。

  1. 导入模块: 在Python代码中,使用import add来导入我们的模块。

  2. 调用函数: 使用add.add(a, b)来调用我们在C代码中定义的add函数。

import add

# 调用C扩展模块中的add函数
result = add.add(1.5, 2.5)

# 打印结果
print(result)  # 输出:4.0

是不是很简单?就这样,我们成功地把C语言写好的“零件”装进了Python的“玩具箱”里,并且让Python可以使用它了。

进阶篇:更复杂的例子和技巧

上面的例子只是一个最简单的演示,实际应用中,我们的C扩展模块可能会更复杂,需要处理更多的数据类型,甚至需要操作Python对象。

1. 处理Python对象

有时候,我们需要在C代码中创建、修改或者返回Python对象。Python C API提供了很多函数来处理Python对象,比如:

  • PyLong_FromLong(long v): 把C语言的long类型转换成Python的int对象。
  • PyUnicode_FromString(const char *v): 把C语言的字符串转换成Python的str对象。
  • PyList_New(Py_ssize_t len): 创建一个新的Python列表。
  • PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item): 设置Python列表的某个元素。

一个例子:创建一个包含指定数量的整数的Python列表。

#include <Python.h>

static PyObject* create_list(PyObject* self, PyObject* args) {
    int length;

    // 解析参数:期望接收一个int类型的参数
    if (!PyArg_ParseTuple(args, "i", &length)) {
        return NULL;
    }

    // 创建一个新的Python列表
    PyObject* list = PyList_New(length);
    if (list == NULL) {
        return NULL; // 创建失败
    }

    // 填充列表
    for (int i = 0; i < length; i++) {
        PyObject* item = PyLong_FromLong(i); // 创建一个Python int对象
        if (item == NULL) {
            Py_DECREF(list); // 释放列表内存
            return NULL;
        }
        PyList_SetItem(list, i, item); // 设置列表的第i个元素,注意:这个函数会偷走item的引用
    }

    return list;
}

static PyMethodDef ListMethods[] = {
    {"create_list",  create_list, METH_VARARGS, "Create a list of integers."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef listmodule = {
    PyModuleDef_HEAD_INIT,
    "list_maker",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    ListMethods
};

PyMODINIT_FUNC
PyInit_list_maker(void)
{
    return PyModule_Create(&listmodule);
}

注意:

  • 引用计数: Python C API使用了引用计数来管理内存。当我们创建一个Python对象时,它的引用计数会增加。当我们不再需要这个对象时,应该使用Py_DECREF(object)来减少它的引用计数。当引用计数变为0时,Python会自动释放这个对象的内存。
  • 错误处理: C代码中要进行错误处理,如果某个函数调用失败,应该返回NULL,并且设置一个错误信息。Python会捕获这个错误,并且抛出一个Python异常。

2. 使用NumPy

如果你要处理大量的数值数据,NumPy是你的好朋友。NumPy提供了一个高效的数组对象,以及很多用于数值计算的函数。

在C扩展中,你可以直接访问NumPy数组的数据,进行高性能的计算。

首先,你需要安装NumPy:

pip install numpy

然后,在你的C代码中包含numpy/arrayobject.h头文件,并且在模块初始化函数中调用import_array()函数。

一个例子:计算NumPy数组的和。

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

static PyObject* sum_array(PyObject* self, PyObject* args) {
    PyObject* array_obj;
    PyArrayObject* array;
    double sum = 0.0;
    int i;

    // 解析参数:期望接收一个NumPy数组
    if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &array_obj)) {
        return NULL;
    }

    // 转换为NumPy数组对象
    array = (PyArrayObject*) array_obj;

    // 获取数组的维度和数据指针
    int ndims = PyArray_NDIM(array);
    npy_intp* dims = PyArray_DIMS(array);
    double* data = (double*) PyArray_DATA(array);

    // 检查数组的维度
    if (ndims != 1) {
        PyErr_SetString(PyExc_ValueError, "Array must be 1-dimensional.");
        return NULL;
    }

    // 计算和
    for (i = 0; i < dims[0]; i++) {
        sum += data[i];
    }

    // 返回结果
    return PyFloat_FromDouble(sum);
}

static PyMethodDef SumMethods[] = {
    {"sum_array",  sum_array, METH_VARARGS, "Sum the elements of a NumPy array."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef summodule = {
    PyModuleDef_HEAD_INIT,
    "sum_numpy",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    SumMethods
};

PyMODINIT_FUNC
PyInit_sum_numpy(void)
{
    import_array(); // 初始化NumPy
    return PyModule_Create(&summodule);
}

注意:

  • import_array(): 必须在模块初始化函数中调用,用来初始化NumPy。
  • PyArray_Type: NumPy数组的类型对象。
  • PyArray_NDIM(array): 获取数组的维度。
  • PyArray_DIMS(array): 获取数组的维度大小。
  • PyArray_DATA(array): 获取数组的数据指针。

3. 调试C扩展

调试C扩展可能会比较困难,因为你需要在C代码中进行调试。

  • 使用gdb: gdb是一个强大的C语言调试器,可以用来调试C扩展。
  • 打印调试信息: 在C代码中使用printf函数打印调试信息,但这可能会影响性能。
  • 使用pdb: Python的调试器pdb也可以用来调试C扩展,但需要一些技巧。

总结:C扩展的优势与不足

优势

  • 性能: C扩展可以显著提高Python代码的性能,尤其是在处理计算密集型的任务时。
  • 访问底层资源: C扩展可以直接访问操作系统底层资源,比如硬件设备。
  • 使用现有的C/C++代码: C扩展可以让你在Python中使用现有的C/C++代码库。

不足

  • 复杂性: 编写C扩展比编写Python代码更复杂,需要了解C语言和Python C API。
  • 可移植性: C扩展的可移植性不如Python代码,因为不同的操作系统可能有不同的C编译器和库。
  • 安全性: C扩展可能会引入安全漏洞,因为C语言没有Python的内存管理和类型检查机制。

表格总结

特性 Python代码 C扩展代码
性能 较低 较高
复杂性 较低 较高
可移植性 较高 较低
安全性 较高 较低
访问底层资源 间接 直接

结尾:选择适合自己的方案

Python C扩展是一个强大的工具,可以用来提高Python代码的性能,访问底层资源,以及使用现有的C/C++代码库。但是,编写C扩展也比较复杂,需要权衡利弊,选择适合自己的方案。

如果你的任务对性能要求不高,或者你只是想快速开发一个原型,那么Python代码可能就足够了。但是,如果你的任务对性能要求很高,或者你需要访问底层资源,那么C扩展可能是一个更好的选择。

总而言之,Python C扩展就像一把瑞士军刀,功能强大,但使用起来也需要一定的技巧。希望今天的讲座能帮助你更好地了解Python C扩展,并且在实际应用中发挥它的威力。

各位,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注