Python调用C函数的三种方式:ctypes、CFFI与C扩展实战指南

📅 2026/7/29 14:57:44
Python调用C函数的三种方式:ctypes、CFFI与C扩展实战指南
1. 项目概述为什么要在Python里调用C函数干了这么多年开发我越来越觉得Python和C就像一对黄金搭档。Python写起来快生态丰富但一碰到计算密集型的活儿比如图像处理、科学计算或者高频交易里的核心算法它的速度就成了瓶颈。这时候C语言的高性能优势就体现出来了。但重写整个项目成本太高。所以在Python里调用C函数就成了一个非常经典且实用的技术方案。简单来说这个技术能让你用Python的胶水特性把C语言这块“性能硬骨头”给粘合进来。你既享受了Python的开发效率和丰富的库又在关键路径上获得了接近原生C的执行速度。这对于做数据分析、量化策略回测、游戏引擎脚本绑定甚至是嵌入式设备的上层控制都意义重大。那么具体怎么“粘合”呢主流有三种方式各有各的适用场景和脾气。今天我就结合自己踩过的坑和实际项目经验把这三种方式掰开揉碎了讲清楚。无论你是刚接触这个领域的Python开发者还是正在为性能优化头疼的工程师这篇文章都能给你一套清晰的路线图。2. 三种核心方式全景解析与选型指南在深入代码之前我们得先搞清楚手头的三把“武器”分别是什么以及什么时候该用哪一把。盲目选型后面可能就是无尽的调试深渊。2.1 方式一ctypes- 轻量级的外交官我把ctypes比作“外交官”。它是Python标准库的一部分这意味着你不需要安装任何额外的东西开箱即用。它的工作方式是直接调用动态链接库在Windows上是.dll在Linux/macOS上是.so中导出的函数。你不需要为C函数写额外的包装代码ctypes负责在Python和C的二进制接口之间进行数据类型转换和调用约定适配。它的核心优势在于“轻”和“快”。轻是指无需编译步骤对于调用现有的、成熟的C库比如某些硬件驱动库、系统API封装库特别方便。快是指上手快几行代码就能完成调用。但外交官也有局限。它主要适用于调用已有的、接口清晰的C库。如果C库的接口非常复杂比如大量使用结构体嵌套、回调函数、复杂的指针操作用ctypes来声明和匹配会非常繁琐且容易出错。它不负责内存管理需要开发者自己小心处理。2.2 方式二CFFI- 灵活的全能选手CFFIC Foreign Function Interface更像一个“全能选手”。它有两种主要模式ABI模式和API模式。ABI模式类似于ctypes也是直接加载动态库。但CFFI的API模式才是其精髓所在它允许你在Python代码中直接书写C语言的函数声明和数据类型定义然后由CFFI在运行时或编译时取决于模式生成高效的绑定代码。CFFI的强大在于其灵活性和对现代C语言特性如inline函数、bool类型的良好支持。它生成的绑定代码通常比ctypes有更好的性能并且接口定义更直观直接写C语法。它同时支持“内联”模式可以把C代码片段直接写在Python脚本里特别适合做原型验证或小型扩展。它的代价是引入了额外的依赖需要安装cffi包并且对于超大型的已有C代码库准备其完整的C声明头文件可能是一项工作量。2.3 方式三Python C扩展 - 深度的系统集成Python C扩展是“系统集成师”。这是最传统、最底层、也是功能最强大的方式。你需要用C语言编写一个符合Python C API规范的模块然后将其编译成一个真正的Python模块.so或.pyd文件。编译后你可以像导入numpy一样import你自己写的这个C扩展模块。这种方式让你能深度介入Python的运行时。你可以定义全新的Python类型类精细控制对象的内存生命周期实现复杂的迭代器协议甚至直接操作Python的虚拟机。性能通常也是最优的因为几乎没有中间转换层。然而它的复杂度最高。你需要学习Python C API自己处理引用计数这是内存安全的关键也是最容易出错的地方并且构建过程需要配置编译工具链setuptools,distutils或现代的meson/scikit-build。它适合构建大型的、需要长期维护的、性能至上的核心库比如NumPy、Pandas的核心部分。选型速查表特性ctypesCFFI(API模式)Python C扩展学习曲线平缓中等陡峭性能较好好极佳功能灵活性较低高极高依赖管理无标准库需安装cffi需C编译器/Python头文件适用场景快速调用现有成熟C库调用复杂C库或混合编程开发高性能核心模块/库内存管理手动半自动/手动手动需遵循Python API3. 核心细节解析与实操要点了解了大局我们得钻到细节里看看。每种方式都有一些“魔鬼细节”不注意的话程序崩溃或者内存泄漏就会找上门。3.1 数据类型的映射跨越语言的鸿沟这是所有跨语言调用最核心、最先要解决的问题。Python中的int、float、str、bytes、list到了C那边对应的是int、double、char*、void*、数组指针。映射错误轻则结果不对重则段错误。ctypes的映射它提供了一系列类型如c_int,c_double,c_char_p,POINTER(c_int)。你需要显式地将Python参数转换为这些ctypes类型或者通过argtypes属性来声明函数的参数类型让ctypes帮你转换。# ctypes 类型声明示例 from ctypes import c_int, c_double, POINTER # 假设C函数 void process_array(int* arr, double factor, int length); my_c_lib.process_array.argtypes [POINTER(c_int), c_double, c_int]这里的关键是POINTER它用于表示C中的指针。对于数组通常需要先创建一个ctypes数组如(c_int * 10)()然后将其传入。CFFI的映射在API模式下你直接写C声明CFFI会自动处理映射。例如你声明int func(int *arr, double factor);CFFI就知道如何将Python的整数列表和浮点数传递过去。它比ctypes更直观因为它用的是C语法。Python C扩展的映射这是最手动但也最可控的。你需要使用PyArg_ParseTuple函数从Python传入的元组中解析出C类型的值以及使用Py_BuildValue将C类型的值打包成Python对象返回。这两个函数使用格式字符串来指定类型转换规则。// C扩展中的类型解析示例 static PyObject* myfunc(PyObject* self, PyObject* args) { int num; double val; char* str; // 解析参数一个整数、一个浮点数、一个字符串 if (!PyArg_ParseTuple(args, ids, num, val, str)) { return NULL; // 解析失败抛出异常 } // ... 使用 num, val, str 进行运算 ... // 返回一个Python浮点数 return Py_BuildValue(d, result); }这里的ids就是格式字符串i代表整型d代表双精度浮点s代表字符串会转换成C的char*。你必须确保传递的Python对象类型匹配并且提供的C变量地址有效。3.2 内存管理的生死线C语言需要手动管理内存而Python有垃圾回收。当两者交汇时内存管理的责任必须清晰否则不是内存泄漏就是悬空指针。ctypes你需要对谁分配内存、谁释放内存心中有数。如果C函数返回一个指针并且这个指针指向它新分配的内存那么通常C库会提供一个配套的释放函数如free_buffer。你必须用ctypes调用这个释放函数否则内存泄漏。如果C函数只是修改你传入的缓冲区那么缓冲区内存通常由Python端ctypes创建和管理。注意当使用c_char_p传递字符串时ctypes会创建一个临时缓冲区。如果C函数保存了这个指针并在后续使用会导致未定义行为。对于需要C函数长期持有的字符串应该用create_string_buffer来分配内存。CFFI在API模式下CFFI提供了ffi.new、ffi.gc等工具来辅助内存管理。ffi.new用于分配内存ffi.gc可以将分配的内存与一个Python对象关联当这个Python对象被垃圾回收时自动调用释放函数。这大大减轻了负担但你需要理解其机制。Python C扩展这是重灾区核心是Python的引用计数。Python C API中每个PyObject*都有一个引用计数。规则很简单当你创建一个新对象如PyLong_FromLong或增加对一个已有对象的引用Py_INCREF时计数加一。当你不再需要一个对象时必须减少其引用Py_DECREF。计数归零时对象被销毁。黄金法则对于从参数args或kwargs中借用的引用你不拥有它不要轻易DECREF。对于你创建并返回的新对象调用者会负责其引用。最常见的错误是DECREF了不该减的对象或者忘了DECREF自己创建的对象前者导致程序崩溃后者导致内存泄漏。3.3 错误处理与异常传递C函数通常通过返回值如-1、NULL或设置全局变量errno来表示错误。Python使用异常。如何把C的错误转换成Python异常ctypes比较原始。你需要检查C函数的返回值然后自己在Python端抛出异常。如果C函数通过errno报错可以用ctypes.get_errno()获取。result my_c_lib.some_operation() if result -1: err ctypes.get_errno() raise OSError(err, os.strerror(err))CFFI支持稍好一些。在调用C函数时如果发生错误如段错误CFFI会将其捕获并转换为一个Python的ffi.error异常。但对于C函数内部的业务逻辑错误仍需像ctypes一样手动处理。Python C扩展这是最规范的方式。Python C API提供了完整的异常机制。当C函数中发生错误时你应该设置一个异常如PyErr_SetString(PyExc_RuntimeError, something wrong)然后返回NULL。Python解释器会看到这个NULL返回值以及被设置的异常然后向上层抛出。if (failure_condition) { PyErr_SetString(PyExc_ValueError, Invalid input value); return NULL; // 函数返回NULL表示异常发生 }这是最符合Python习惯的方式调用者可以用try...except来捕获。4. 实操过程与核心环节实现理论说了这么多不动手都是空谈。我们分别用三种方式来实现一个相同的功能一个计算数组平均值的C函数。假设我们有一个编译好的动态库libavg.soLinux/macOS或avg.dllWindows其中包含函数double average(double* arr, int n)。4.1 使用ctypes进行调用首先确保动态库在系统路径或当前目录下。# ctypes_demo.py import ctypes import sys import os # 1. 加载动态库 # 根据平台选择不同的加载方式和库文件名 if sys.platform.startswith(win): lib_path ./avg.dll loader ctypes.WinDLL elif sys.platform.startswith(darwin): lib_path ./libavg.dylib # macOS loader ctypes.CDLL else: lib_path ./libavg.so # Linux loader ctypes.CDLL try: avg_lib loader(lib_path) except OSError as e: print(f无法加载库 {lib_path}: {e}) # 可以尝试在系统路径中查找 # avg_lib ctypes.CDLL(avg) # 如果库在系统路径如 /usr/lib sys.exit(1) # 2. 指定函数参数和返回类型 # 这步不是必须的但强烈建议做能让ctypes进行类型检查和安全转换 avg_lib.average.argtypes [ctypes.POINTER(ctypes.c_double), ctypes.c_int] avg_lib.average.restype ctypes.c_double # 3. 准备数据并调用 def compute_average_py(data_list): 使用ctypes调用C函数计算平均值 n len(data_list) # 将Python列表转换为C的double数组 # 方法1: 使用 (c_double * n) 创建数组类型并实例化 ArrType ctypes.c_double * n c_array ArrType(*data_list) # 解包列表传入 # 方法2: 使用cast (更灵活适用于已有buffer) # import array # py_array array.array(d, data_list) # c_array (ctypes.c_double * n).from_buffer(py_array) # 调用C函数 result avg_lib.average(c_array, n) return result # 4. 测试 if __name__ __main__: test_data [1.0, 2.5, 3.5, 4.0, 5.5] py_avg sum(test_data) / len(test_data) c_avg compute_average_py(test_data) print(fPython计算的平均值: {py_avg}) print(fC函数计算的平均值: {c_avg}) print(f两者是否接近: {abs(py_avg - c_avg) 1e-10})关键点解析库加载跨平台时需要注意库文件后缀和加载器CDLL用于标准C调用约定cdeclWinDLL用于Windows的stdcall大多数现代C库用cdecl。类型声明argtypes和restype的声明至关重要它能防止传入错误类型的参数并确保返回值被正确解释。数组传递(ctypes.c_double * n)创建了一个长度为n的c_double数组类型然后实例化。*data_list将Python列表解包作为初始化参数。这是传递数组最直接的方式。4.2 使用CFFI(API模式) 进行调用首先安装cffipip install cffi。我们采用API模式它需要编译一个小的扩展模块但性能更好。# cffi_demo.py import sys sys.path.insert(0, .) # 确保能导入下面编译生成的模块 # 1. 导入CFFI并创建FFI对象 from cffi import FFI ffi FFI() # 2. 声明C函数原型直接写C代码 ffi.cdef( double average(double* arr, int n); ) # 3. 设置源文件这里我们直接链接已编译的库 # 如果是简单的C函数也可以把C源码直接写在这里用 ffi.set_source 编译。 # 这里我们使用 ffi.dlopen 直接加载动态库类似于ctypes。 lib_path ./libavg.so # 根据你的平台修改 try: # 4. 加载动态库获取C函数 C ffi.dlopen(lib_path) except OSError as e: print(f无法加载库 {lib_path}: {e}) sys.exit(1) # 5. 包装调用函数 def compute_average_cffi(data_list): n len(data_list) # 使用ffi.new分配一个C数组 # ffi.new分配的内存会在Python对象生命周期结束后被垃圾回收如果未被C代码长期引用 c_array ffi.new(double[], n) # 等同于 double c_array[n]; # 将Python列表数据复制到C数组 for i, val in enumerate(data_list): c_array[i] val # 调用C函数 result C.average(c_array, n) return result # 6. 测试 if __name__ __main__: test_data [1.0, 2.5, 3.5, 4.0, 5.5] py_avg sum(test_data) / len(test_data) c_avg compute_average_cffi(test_data) print(fPython计算的平均值: {py_avg}) print(fC函数计算的平均值: {c_avg}) print(f两者是否接近: {abs(py_avg - c_avg) 1e-10}) # 更“CFFI”的方式使用ffi.from_buffer (如果数据已经是某种buffer) import array py_array array.array(d, test_data) # d 表示双精度浮点 # from_buffer 创建了一个C指针指向Python数组的内存避免复制 c_ptr ffi.cast(double*, ffi.from_buffer(py_array)) result2 C.average(c_ptr, len(test_data)) print(f使用from_buffer的结果: {result2})关键点解析ffi.cdef这是核心你用纯C语法声明要调用的函数和可能用到的结构体。CFFI会根据这个声明来生成正确的调用代码。ffi.new在C堆上分配内存返回一个指向该内存的cdata对象。当这个cdata对象在Python中失去所有引用时内存会被自动释放。这比ctypes的手动管理方便。ffi.from_buffer这是一个性能优化技巧。如果数据已经存在于某个支持缓冲区协议buffer protocol的Python对象中如array.array,bytes,numpy.ndarrayfrom_buffer可以零拷贝地获取一个指向该数据内存的C指针。但要注意在C函数操作数据期间必须确保原始的Python对象不被垃圾回收或改变大小。4.3 使用Python C扩展实现这是最复杂但最强大的方式。我们需要编写一个C源文件并将其编译成Python模块。第一步编写C扩展源码 (avgmodule.c)#define PY_SSIZE_T_CLEAN #include Python.h /* 实际的C计算函数 */ static double c_average(double* arr, int n) { double sum 0.0; for (int i 0; i n; i) { sum arr[i]; } return n 0 ? sum / n : 0.0; } /* Python模块的包装函数 */ static PyObject* py_average(PyObject* self, PyObject* args) { PyObject* py_list; /* 解析一个Python列表对象 */ if (!PyArg_ParseTuple(args, O!, PyList_Type, py_list)) { PyErr_SetString(PyExc_TypeError, 参数必须是一个列表); return NULL; } Py_ssize_t n PyList_Size(py_list); if (n INT_MAX) { // 检查长度是否超出C int范围 PyErr_SetString(PyExc_OverflowError, 列表太长); return NULL; } /* 将Python列表转换为C数组 */ double* c_arr (double*)PyMem_Malloc(n * sizeof(double)); if (c_arr NULL) { PyErr_SetString(PyExc_MemoryError, 内存分配失败); return NULL; } for (Py_ssize_t i 0; i n; i) { PyObject* item PyList_GetItem(py_list, i); // “借用”引用无需DECREF if (!PyFloat_Check(item)) { PyMem_Free(c_arr); PyErr_SetString(PyExc_TypeError, 列表元素必须为浮点数); return NULL; } c_arr[i] PyFloat_AsDouble(item); if (PyErr_Occurred()) { // 检查转换是否出错如溢出 PyMem_Free(c_arr); return NULL; } } /* 调用C计算函数 */ double result c_average(c_arr, (int)n); /* 释放临时数组内存 */ PyMem_Free(c_arr); /* 将C double 转换为 Python float 并返回 */ return PyFloat_FromDouble(result); } /* 方法定义表 */ static PyMethodDef AvgMethods[] { {average, py_average, METH_VARARGS, 计算一个浮点数列表的平均值。}, {NULL, NULL, 0, NULL} /* 哨兵表示结束 */ }; /* 模块定义结构 */ static struct PyModuleDef avgmodule { PyModuleDef_HEAD_INIT, avg, /* 模块名 */ NULL, /* 模块文档可以为NULL */ -1, /* 每个解释器状态的模块内存大小-1表示全局状态 */ AvgMethods /* 方法表 */ }; /* 模块初始化函数 */ PyMODINIT_FUNC PyInit_avg(void) { return PyModule_Create(avgmodule); }第二步编写setup.py用于编译# setup.py from setuptools import setup, Extension # 定义扩展模块 avg_module Extension( avg, # 模块名必须和C源码中 PyInit_avg 里的名字一致 sources[avgmodule.c], # 可以在这里添加额外的编译参数和库 # extra_compile_args[-O2, -marchnative], # 优化选项 # libraries[m], # 链接数学库如果需要 ) setup( nameavg_example, version0.1, description一个计算平均值的C扩展示例, ext_modules[avg_module], )第三步编译并安装模块在命令行中进入该目录运行pip install . # 或者 python setup.py build_ext --inplace--inplace参数会在当前目录生成编译好的模块文件如avg.cpython-39-x86_64-linux-gnu.so方便测试。第四步在Python中使用# test_cext.py import avg # 导入我们编译的C扩展模块 test_data [1.0, 2.5, 3.5, 4.0, 5.5] result avg.average(test_data) print(fC扩展计算的平均值: {result})关键点解析PyArg_ParseTuple这是解析Python参数的瑞士军刀。O!表示期望一个特定类型的对象后面跟着类型对象和存储变量的地址。这里我们要求参数是一个PyList_TypePython列表。引用计数与借用PyList_GetItem返回的是一个“借用引用”borrowed reference我们不需要负责它的DECREF。这很重要如果错误地DECREF了会导致程序崩溃。内存分配使用PyMem_Malloc和PyMem_Free来分配和释放内存。这使用了Python自己的内存分配器与Python的内存管理机制更协调。错误处理每一步都可能失败类型错误、内存不足、数值溢出。一旦检测到错误我们设置异常PyErr_SetString并返回NULL。在返回NULL前必须清理已分配的资源如PyMem_Free(c_arr)否则会内存泄漏。模块初始化PyInit_avg函数是模块的入口点其名称必须与模块名avg以及Extension中定义的名称匹配。5. 常见问题与排查技巧实录在实际项目中调用C函数很少有一帆风顺的。下面是我总结的几个最常见的问题和排查手段。5.1 段错误Segmentation Fault这是最令人头疼的错误意味着程序访问了不该访问的内存。ctypes/CFFI中常见原因指针类型声明错误C函数期望int*你传了一个int的ctypes类型或者CFFI声明不匹配。数组长度传递错误C函数需要数组长度n你传的值比实际数组大小大或小导致越界访问。生命周期问题你传递了一个指向临时Python字符串c_char_p的指针给C函数C函数保存了这个指针并在Python字符串被回收后使用它。调用约定不匹配Windows上某些旧的API使用__stdcall而ctypes.CDLL默认用于__cdecl。需要用ctypes.WinDLL或显式设置argtypes的__stdcall属性。排查技巧简化复现创建一个最小的、能复现错误的测试用例。使用调试器用gdbLinux或lldbmacOS运行Python脚本在段错误发生时查看堆栈回溯bt能精确定位到C代码中出错的行。打印日志在C函数内部关键位置添加printf或fprintf(stderr, ...)观察执行流程和变量值。检查argtypes/restype确保ctypes中的声明与C头文件完全一致包括指针类型、结构体定义。5.2 导入错误ImportError或库加载失败ctypes/CFFI报OSError: cannot find library路径问题库文件不在系统库路径如/usr/lib,/usr/local/lib或当前工作目录。使用绝对路径或修改LD_LIBRARY_PATHLinux、DYLD_LIBRARY_PATHmacOS、PATHWindows环境变量。依赖缺失你的动态库依赖其他库可以用ldd libavg.so或otool -L libavg.dylib查看而这些依赖没有安装。架构不匹配在macOS上可能遇到x86_64和arm64不兼容的问题。确保Python解释器和C库的架构一致。C扩展编译失败缺少Python.h安装Python开发包如python3-devDebian/Ubuntu或python3-develFedora/RHEL。编译器错误检查C代码语法确保符合C99/C11标准。复杂的扩展可能需要特定的编译标志。5.3 内存泄漏Memory Leak程序运行时间长了内存占用不断增长。C扩展中的泄漏这是重灾区。反复检查PyMem_Malloc/PyMem_New等分配操作是否有对应的PyMem_Free/PyMem_Del。尤其注意在错误处理路径return NULL之前也要释放资源。ctypes中的泄漏如果C函数内部调用了malloc并且返回了指针你必须调用对应的C释放函数如free来释放。ctypes不会帮你做这件事。排查工具ValgrindLinux强大的内存调试工具能检测泄漏、非法内存访问等。用valgrind --leak-checkfull python your_script.py运行。tracemallocPython标准库可以跟踪Python对象的内存分配但对于C扩展分配的内存可能无能为力。5.4 性能未达预期明明调用了C函数为什么速度提升不明显数据拷贝开销这是最大的性能杀手。如果你在Python端准备了一个大列表然后通过ctypes或CFFI逐个元素复制到C数组这个复制过程本身就很耗时。优化策略使用numpy数组。numpy数组在内存中是连续的、类型化的并且支持缓冲区协议。ctypes可以通过numpy.ctypeslib.as_ctypes直接获取指针CFFI可以用ffi.from_buffer零拷贝获取指针。这是性能最优的方案。# 使用numpy与ctypes配合 import numpy as np import ctypes data_np np.array([1.0, 2.0, 3.0], dtypenp.float64) # 获取指针无需拷贝 c_array_ptr data_np.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) result my_c_lib.func(c_array_ptr, len(data_np))调用开销对于非常小的、被频繁调用的C函数比如在循环最内层跨语言调用的开销参数打包、解包可能抵消掉C本身的性能优势。优化策略避免在紧密循环中频繁调用微小的C函数。尽量将循环逻辑也移到C端让一次C调用完成更多工作即“向量化”操作。5.5 多线程与GIL全局解释器锁Python有GIL同一时刻只有一个线程可以执行Python字节码。但C扩展可以释放GIL。危险如果你的C函数会长时间运行如复杂计算、I/O等待并且你不释放GIL它会阻塞所有其他Python线程。解决方案在C扩展中在进入纯C计算部分前使用Py_BEGIN_ALLOW_THREADS宏释放GIL计算完成后用Py_END_ALLOW_THREADS重新获取。static PyObject* py_long_running_func(PyObject* self, PyObject* args) { // ... 解析参数 ... Py_BEGIN_ALLOW_THREADS // 释放GIL允许其他Python线程运行 // 这里是耗时的C计算或I/O操作 long_running_c_operation(); Py_END_ALLOW_THREADS // 重新获取GIL // ... 构建返回值 ... }注意在GIL释放期间绝对不能调用任何Python C API函数如PyFloat_FromDouble也不能访问或修改任何Python对象。只能操作纯C的数据。6. 进阶考量与工具链选择当项目变大或者需要更现代化的开发体验时基础的setup.py可能显得力不从心。这时可以考虑更先进的工具。6.1 现代构建工具scikit-buildCMake对于复杂的C/C扩展项目特别是那些本身就用CMake构建的库scikit-build是更好的选择。它是setuptools的替代品使用CMake作为构建后端。优势标准化CMake是C/C生态的事实标准能处理复杂的依赖、编译选项和跨平台构建。集成性好可以方便地打包和分发依赖的第三方C库。性能CMake支持并行编译构建速度更快。一个简单的pyproject.toml配置示例[build-system] requires [scikit-build-core, cmake] build-backend scikit_build_core.build [project] name my_fast_module version 0.0.1 [tool.scikit-build] cmake.args [-DCMAKE_BUILD_TYPERelease]6.2 类型安全的桥梁PyO3(Rust) 与pybind11(C)如果你在使用Rust或C有更高级的绑定生成器。PyO3用于创建Python的Rust扩展。Rust的内存安全特性可以极大避免C扩展中常见的悬空指针、内存泄漏等问题。它提供了非常直观的宏让你能用Rust语法定义Python模块和类。use pyo3::prelude::*; #[pyfunction] fn average(numbers: Vecf64) - PyResultf64 { let sum: f64 numbers.iter().sum(); Ok(sum / numbers.len() as f64) } #[pymodule] fn my_rust_module(_py: Python, m: PyModule) - PyResult() { m.add_function(wrap_pyfunction!(average, m)?)?; Ok(()) }pybind11一个轻量级的C库用于创建Python绑定。它的API设计深受Boost.Python启发但只有头文件没有依赖。它自动处理了很多Python C API的细节比如引用计数、异常转换代码写起来更简洁安全。#include pybind11/pybind11.h #include vector namespace py pybind11; double average(const std::vectordouble v) { double sum 0.0; for (auto x : v) sum x; return v.empty() ? 0.0 : sum / v.size(); } PYBIND11_MODULE(my_cpp_module, m) { m.doc() pybind11 example plugin; m.def(average, average, 计算向量的平均值); }这两种工具都极大地提升了开发体验和代码安全性是新建高性能扩展项目的优先选择。6.3 调试技巧让问题无处遁形在C扩展中打印调试信息除了用printf在C扩展中更推荐使用PySys_WriteStderr它直接写入Python的sys.stderr。使用Python的faulthandler模块在程序开头import faulthandler; faulthandler.enable()。当发生段错误等严重错误时它会打印出Python的堆栈信息对于定位问题发生在哪个Python调用中非常有帮助。单元测试为你的C扩展编写Python单元测试使用unittest或pytest。这不仅能保证功能正确在修改代码后也能快速回归。测试应覆盖正常输入、边界条件空列表、超大数值和错误输入错误类型。