Python高级技术之:`Python`的`C`扩展:如何编写和编译`Python`与`C`交互的模块。

各位老铁,今天咱们来聊聊Python的高级玩法:用C扩展Python,让你的Python代码像打了鸡血一样飞起来!

开场白:Python虽好,但有时也得C来帮忙

Python这门语言,上手容易,库也多,简直是编程界的瑞士军刀。但是,有些时候,Python的性能会成为瓶颈。比如,需要进行大量的数值计算、图像处理,或者要调用一些底层硬件接口的时候,Python就有点力不从心了。这时候,我们就需要请出我们的秘密武器——C语言。

C语言以其运行效率高、控制力强而闻名。通过C扩展,我们可以把一些性能敏感的代码用C语言编写,然后让Python来调用,这样既能享受到Python的便利性,又能保证程序的运行效率。

第一部分:为什么要用C扩展?

在深入技术细节之前,咱们先来聊聊为什么要用C扩展,而不是用其他方法优化Python代码。

  • 性能!性能!还是性能!

    这是最主要的原因。C语言编译后的代码直接运行在硬件上,效率比Python解释器高得多。对于一些计算密集型的任务,C扩展可以带来数量级的性能提升。

  • 访问底层资源

    Python是高级语言,对底层硬件的访问能力有限。而C语言可以直接访问内存、寄存器等底层资源,可以实现一些Python无法实现的功能。

  • 复用现有的C/C++代码

    很多优秀的库是用C/C++编写的。通过C扩展,我们可以直接在Python中使用这些库,而不需要重新编写。

  • 保护你的代码

    Python是解释型语言,源代码容易被反编译。而C语言编译后的代码是二进制文件,更难被破解。当然,这只是相对的,没有绝对的安全。

第二部分:准备工作

在开始编写C扩展之前,我们需要做一些准备工作。

  1. 安装Python开发包

    在Linux系统上,通常需要安装python-devpython3-dev包。在Windows系统上,需要安装Visual Studio,并确保Python的include目录和libs目录已经添加到Visual Studio的搜索路径中。

    例如在Ubuntu系统上,你可以使用以下命令安装:

    sudo apt-get update
    sudo apt-get install python3-dev
  2. 安装必要的构建工具

    我们需要使用setuptools或者distutils来构建我们的C扩展模块。通常情况下,Python已经自带了setuptools,如果没有,可以使用pip安装。

    pip install setuptools

第三部分:编写C扩展

接下来,咱们来编写一个简单的C扩展模块,实现一个加法函数。

  1. 创建C源文件

    创建一个名为my_module.c的文件,内容如下:

    #include <Python.h>
    
    // C 函数:两个整数相加
    static PyObject* my_module_add(PyObject* self, PyObject* args) {
        int a, b;
    
        // 从 Python 传递的参数中解析出两个整数
        if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
            return NULL; // 参数解析失败,返回 NULL
        }
    
        // 计算结果
        int result = a + b;
    
        // 将结果转换为 Python 对象并返回
        return PyLong_FromLong(result);
    }
    
    // 方法列表:定义模块中可用的函数
    static PyMethodDef MyModuleMethods[] = {
        {"add",  my_module_add, METH_VARARGS, "Adds two integers."},
        {NULL, NULL, 0, NULL}        /* Sentinel */
    };
    
    // 模块定义结构体
    static struct PyModuleDef mymodule = {
        PyModuleDef_HEAD_INIT,
        "my_module",   // 模块名
        "A sample module", // 模块文档
        -1,       /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
        MyModuleMethods
    };
    
    // 模块初始化函数
    PyMODINIT_FUNC PyInit_my_module(void) {
        return PyModule_Create(&mymodule);
    }

    代码解释:

    • #include <Python.h>: 必须包含的头文件,包含了Python C API的定义。
    • my_module_add: 这是一个C函数,实现了加法功能。它接受两个参数:selfargsself通常不用管,args是一个Python元组,包含了从Python传递过来的参数。
    • PyArg_ParseTuple: 这个函数用于从args中解析出参数。"ii"表示期望解析两个整数,&a&b是用于存储解析结果的变量的地址。如果解析失败,返回NULL
    • PyLong_FromLong: 这个函数用于将C语言的整数转换为Python的整数对象。
    • MyModuleMethods: 这是一个方法列表,定义了模块中可用的函数。每一项包含四个元素:函数名、C函数指针、参数类型、文档字符串。最后一项必须是{NULL, NULL, 0, NULL},作为哨兵。METH_VARARGS 表示该函数接受任意个数的位置参数。
    • PyModuleDef: 模块定义结构体,包含了模块的名称、文档、方法列表等信息。
    • PyInit_my_module: 模块初始化函数。这个函数在Python导入模块时被调用。它的名称必须是PyInit_加上模块名。它返回一个指向模块对象的指针。
  2. 创建setup.py文件

    创建一个名为setup.py的文件,内容如下:

    from setuptools import setup, Extension
    
    module1 = Extension('my_module',
                        sources = ['my_module.c'])
    
    setup (name = 'MyModule',
           version = '1.0',
           description = 'This is a demo package',
           ext_modules = [module1])

    代码解释:

    • from setuptools import setup, Extension: 导入setupExtension类。
    • Extension: 创建一个Extension对象,用于描述C扩展模块。第一个参数是模块名,第二个参数是源文件列表。
    • setup: 调用setup函数,用于构建和安装C扩展模块。name是包名,version是版本号,description是描述信息,ext_modules是C扩展模块列表。

第四部分:编译和安装C扩展

在命令行中,进入包含setup.py文件的目录,执行以下命令:

python setup.py build_ext --inplace

这个命令会编译C扩展模块,并在当前目录下生成一个my_module.so (Linux) 或 my_module.pyd (Windows) 文件。--inplace选项表示在当前目录下生成文件,而不是在build目录下。

如果你想安装到Python的site-packages目录下,可以使用以下命令:

python setup.py install

这需要管理员权限。

第五部分:在Python中使用C扩展

现在,我们可以在Python中使用我们编写的C扩展模块了。

import my_module

# 调用C扩展模块中的 add 函数
result = my_module.add(10, 20)
print(result)  # 输出: 30

第六部分:更复杂的例子:操作Python列表

咱们再来一个更复杂的例子,演示如何在C扩展中操作Python列表。

  1. 修改C源文件

    修改my_module.c文件,添加一个函数,用于计算Python列表中所有元素的和。

    #include <Python.h>
    
    // C 函数:两个整数相加
    static PyObject* my_module_add(PyObject* self, PyObject* args) {
        int a, b;
    
        // 从 Python 传递的参数中解析出两个整数
        if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
            return NULL; // 参数解析失败,返回 NULL
        }
    
        // 计算结果
        int result = a + b;
    
        // 将结果转换为 Python 对象并返回
        return PyLong_FromLong(result);
    }
    
    // C 函数:计算 Python 列表中所有元素的和
    static PyObject* my_module_sum_list(PyObject* self, PyObject* args) {
        PyObject *listObj;
        long sum = 0;
        int i, listSize;
    
        // 从 Python 传递的参数中解析出列表对象
        if (!PyArg_ParseTuple(args, "O", &listObj)) {
            return NULL; // 参数解析失败,返回 NULL
        }
    
        // 检查是否是列表对象
        if (!PyList_Check(listObj)) {
            PyErr_SetString(PyExc_TypeError, "Argument must be a list");
            return NULL;
        }
    
        // 获取列表大小
        listSize = PyList_Size(listObj);
    
        // 遍历列表,计算所有元素的和
        for (i = 0; i < listSize; i++) {
            PyObject *item = PyList_GetItem(listObj, i); // 不增加引用计数
            long value = PyLong_AsLong(item);
    
            if (PyErr_Occurred()) {
                return NULL; // 转换失败,返回 NULL
            }
    
            sum += value;
        }
    
        // 将结果转换为 Python 对象并返回
        return PyLong_FromLong(sum);
    }
    
    // 方法列表:定义模块中可用的函数
    static PyMethodDef MyModuleMethods[] = {
        {"add",  my_module_add, METH_VARARGS, "Adds two integers."},
        {"sum_list",  my_module_sum_list, METH_VARARGS, "Calculates the sum of a list."},
        {NULL, NULL, 0, NULL}        /* Sentinel */
    };
    
    // 模块定义结构体
    static struct PyModuleDef mymodule = {
        PyModuleDef_HEAD_INIT,
        "my_module",   // 模块名
        "A sample module", // 模块文档
        -1,       /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */
        MyModuleMethods
    };
    
    // 模块初始化函数
    PyMODINIT_FUNC PyInit_my_module(void) {
        return PyModule_Create(&mymodule);
    }

    代码解释:

    • my_module_sum_list: 这是一个C函数,实现了计算Python列表中所有元素和的功能。
    • PyArg_ParseTuple(args, "O", &listObj)"O" 表示期望解析一个Python对象。
    • PyList_Check: 检查listObj是否是列表对象。
    • PyList_Size: 获取列表的大小。
    • PyList_GetItem: 获取列表中指定索引的元素。注意:这个函数返回的是一个 borrowed reference,也就是不增加引用计数。 如果需要长期持有这个对象,需要调用 Py_INCREF 增加引用计数,用完后调用 Py_DECREF 减少引用计数,防止内存泄漏。
    • PyLong_AsLong: 将Python的整数对象转换为C语言的整数。
    • PyErr_Occurred: 检查是否发生了错误。如果发生了错误,返回非NULL
    • PyExc_TypeError: 一个Python异常类型,表示类型错误。
    • PyErr_SetString: 设置一个Python异常信息。
  2. 重新编译和安装C扩展

    执行以下命令重新编译和安装C扩展:

    python setup.py build_ext --inplace
  3. 在Python中使用C扩展

    import my_module
    
    # 调用C扩展模块中的 sum_list 函数
    my_list = [1, 2, 3, 4, 5]
    result = my_module.sum_list(my_list)
    print(result)  # 输出: 15

第七部分:错误处理

在C扩展中,错误处理非常重要。如果C代码发生错误,需要将错误信息传递给Python,让Python能够正确处理。

  • 设置异常

    可以使用PyErr_SetString函数设置异常信息。第一个参数是异常类型,第二个参数是错误信息。例如:

    PyErr_SetString(PyExc_TypeError, "Invalid argument type");
    return NULL;
  • 返回NULL

    如果C函数发生错误,应该返回NULL。Python解释器会检查返回值是否为NULL,如果是,就会抛出一个异常。

  • 清理资源

    在C函数返回之前,需要清理所有分配的资源,例如内存、文件句柄等。

第八部分:引用计数

Python使用引用计数来管理内存。每个Python对象都有一个引用计数,表示有多少个指针指向这个对象。当引用计数为0时,对象就会被释放。

在C扩展中,需要特别注意引用计数的管理。如果忘记增加或减少引用计数,可能会导致内存泄漏或程序崩溃。

  • 增加引用计数

    可以使用Py_INCREF函数增加引用计数。例如:

    Py_INCREF(obj);
  • 减少引用计数

    可以使用Py_DECREF函数减少引用计数。例如:

    Py_DECREF(obj);
  • Borrowed reference

    有些函数返回的是 borrowed reference,也就是不增加引用计数。例如,PyList_GetItem函数返回的就是 borrowed reference。如果需要长期持有这个对象,需要调用 Py_INCREF 增加引用计数,用完后调用 Py_DECREF 减少引用计数。

第九部分:一些建议

  • 尽量使用现有的库

    如果已经有现成的C/C++库实现了你需要的功能,尽量使用它们,而不是自己重新编写。

  • 保持C代码的简洁

    C代码应该尽量简洁、易懂。不要过度优化,避免使用复杂的技巧。

  • 编写测试用例

    为C扩展编写测试用例,确保其功能正确。

  • 使用代码分析工具

    使用代码分析工具,例如Valgrind,检查内存泄漏和错误。

总结

C扩展是Python的一项高级技术,可以显著提高Python程序的性能。但是,C扩展的编写比较复杂,需要掌握C语言和Python C API。希望通过今天的讲座,大家对C扩展有了更深入的了解。 以后有机会再给大家分享更多关于Python高级技术的知识。 感谢大家的聆听!

发表回复

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