当前位置: 首页 > news >正文

Python C扩展开发实战:从性能瓶颈到原生加速

1. 项目概述:为什么我们需要用C来扩展Python?

如果你写过Python,大概率经历过这样的时刻:一个数据处理脚本,逻辑清晰,但跑起来就是慢得让人心焦。你尝试了各种优化,用了NumPy,甚至上了多进程,但性能瓶颈依然卡在那里,像一道无法逾越的鸿沟。这时,一个声音可能会在你脑海中响起:“要是能用C来写这部分核心逻辑就好了。”没错,这就是我们今天要聊的——用C语言为Python编写扩展模块。这绝不是为了炫技,而是一个在特定场景下,解决性能问题的“杀手锏”。它能让你在享受Python开发效率的同时,在关键路径上获得接近原生C的性能。无论是高频交易中的实时计算、游戏引擎中的物理模拟,还是科学计算中的矩阵运算,当你需要榨干硬件最后一丝性能时,C扩展就成了必经之路。这篇文章,我将以一个过来人的身份,带你从零开始,手把手搭建环境、编写第一个扩展,并深入到内存管理、线程安全、性能调优等高级话题,帮你避开我当年踩过的所有坑。

2. 环境准备与工具链搭建

2.1 核心工具选择与配置

工欲善其事,必先利其器。编写C扩展,你首先需要一个可靠的开发环境。对于大多数开发者,我强烈推荐Visual Studio Code (VSCode)配合MSVC (Windows)GCC/Clang (Linux/macOS)的组合。VSCode的C/C++扩展提供了出色的代码补全、调试和项目管理体验。

在Windows上,最省心的方式是安装Visual Studio Build Tools或完整版的Visual Studio,并确保勾选“使用C++的桌面开发”工作负载,这会自动安装MSVC编译器和必要的Windows SDK。之后,你需要在系统环境变量中确认cl.exe(MSVC编译器)的路径已加入PATH。在Linux(如Ubuntu)上,一条命令即可搞定基础环境:sudo apt-get install build-essential python3-dev。macOS用户则可以通过Xcode Command Line Tools获取Clang编译器:xcode-select --install

Python方面,你需要的不只是Python解释器,更重要的是Python开发头文件(Python.h)库文件。这正是python3-dev包(Linux)或通过官方安装器(Windows/macOS)安装Python时勾选“安装开发工具”选项的作用。你可以通过一个简单的命令来测试环境是否就绪:打开终端,尝试python3-config --includes(Unix-like系统)或查看Python安装目录下的include文件夹,确认Python.h文件存在。

注意:不同Python版本(如3.8, 3.9, 3.10)的头文件可能有细微差别。务必确保你编译扩展时使用的Python头文件版本,与运行时使用的Python解释器版本完全一致,否则可能导致难以排查的崩溃或导入错误。

2.2 项目结构与构建系统入门

一个清晰的目录结构能让后续开发事半功倍。我建议采用如下结构:

my_cextension/ ├── src/ │ ├── mymodule.c # C扩展核心源代码 │ └── mymodule.h # 头文件(可选,用于复杂项目) ├── setup.py # 构建与安装脚本 ├── tests/ # 测试代码 └── README.md

其中,setup.py是整个项目的构建心脏。它使用Python的setuptools(或传统的distutils)模块来定义如何编译你的C代码。一个最基础的setup.py示例如下:

from setuptools import setup, Extension # 定义扩展模块 module = Extension( 'mymodule', # 未来在Python中导入的名字:import mymodule sources=['src/mymodule.c'], # C源文件列表 include_dirs=[], # 额外的头文件搜索路径 library_dirs=[], # 额外的库文件搜索路径 libraries=[], # 需要链接的库,如 ['m'] 链接数学库 ) setup( name='my_cextension', version='0.1.0', description='一个高性能的C语言Python扩展示例', ext_modules=[module], # 将我们定义的扩展模块加入列表 )

有了这个文件,你就可以在项目根目录下,通过命令行执行pip install .来进行“就地”开发安装,或者执行python setup.py build_ext --inplace来只编译生成扩展模块文件(通常是.pyd.so文件),而不进行全局安装,便于快速测试。

3. C扩展核心机制与第一个“Hello World”

3.1 Python C API 初窥

Python解释器本身是由C写的,它提供了一整套丰富的C API,允许C代码与Python对象世界进行交互。所有扩展模块的入口,都必须遵循一个固定的模板。这个模板的核心是定义一个模块方法表和一个模块定义结构体

首先,每个你想暴露给Python的函数,都需要一个对应的C函数。这个C函数的签名是固定的:PyObject* Py_func(PyObject* self, PyObject* args)。它返回一个Python对象指针,接受两个参数:self(对于模块级函数,通常是NULL;对于类方法,是实例对象)和args(一个包含所有传入参数的元组)。

接着,你需要用一个PyMethodDef结构体数组来声明这些函数,告诉Python每个C函数对应的Python函数名、调用方式以及文档字符串。

最后,用一个PyModuleDef结构体来定义模块本身,并将方法表关联上去。

3.2 手写第一个扩展:helloworld

让我们跳过抽象描述,直接看代码。下面是一个完整的helloworld.c,它实现了一个greet(name)函数,返回一句问候语。

#define PY_SSIZE_T_CLEAN #include <Python.h> /* 1. 实现具体的C函数 */ static PyObject* helloworld_greet(PyObject* self, PyObject* args) { const char* name; // 用于接收Python传过来的字符串参数 // 解析Python传递的参数。格式字符串"s"表示期望一个字符串。 // 函数将解析结果存入我们提供的变量地址 &name 中。 if (!PyArg_ParseTuple(args, "s", &name)) { return NULL; // 如果解析失败,返回NULL,Python层会抛出TypeError异常。 } // 构造返回的字符串。Py_BuildValue 用于将C变量构建成Python对象。 // 格式字符串"s"表示构建一个字符串,后面的参数是C字符串。 return Py_BuildValue("s", "Hello from C, "); } /* 2. 定义模块的方法表 */ static PyMethodDef HelloworldMethods[] = { { "greet", // 在Python中调用的方法名 helloworld_greet, // 对应的C函数指针 METH_VARARGS, // 调用约定,表示使用 PyArg_ParseTuple 解析参数 "Say hello to someone." // 文档字符串 }, {NULL, NULL, 0, NULL} // 哨兵,表示方法表结束 }; /* 3. 定义模块结构 */ static struct PyModuleDef helloworldmodule = { PyModuleDef_HEAD_INIT, // 必须的宏,初始化模块定义头部 "helloworld", // 模块名 NULL, // 模块文档字符串(可NULL) -1, // 模块状态大小(-1表示全局状态) HelloworldMethods // 上面定义的方法表 }; /* 4. 模块初始化函数(入口点) */ PyMODINIT_FUNC PyInit_helloworld(void) { return PyModule_Create(&helloworldmodule); }

编译并测试它。在包含setup.pysrc/helloworld.c的目录下运行:

python setup.py build_ext --inplace

如果成功,会生成一个类似helloworld.cpython-39-win_amd64.pyd的文件。然后在同一目录启动Python解释器:

import helloworld print(helloworld.greet("Alice")) # 输出:b'Hello from C, Alice!'

恭喜!你的第一个C扩展模块已经运行起来了。虽然它很简单,但已经包含了参数解析、对象构建、模块注册等所有核心环节。

4. 数据类型转换与内存管理陷阱

4.1 参数解析:PyArg_ParseTuplePy_BuildValue

PyArg_ParseTuplePy_BuildValue是你与Python世界通信的“翻译官”。它们的第一个参数都是格式字符串,指定了后续参数的类型和转换方式。

常用格式符:

  • sz: C字符串 (char*)。s会检查字符串是否包含空字符,z允许为NULL
  • i,l: C整型 (int,long)。
  • f,d: C浮点型 (float,double)。
  • O: 一个Python对象 (PyObject*)。通常需要进一步类型检查。
  • O!: 一个特定类型的Python对象(如O!i,&PyLong_Type表示期望一个int对象)。
  • s#,y#: 字符串/字节串及其长度 (char*, Py_ssize_t)。
  • (...): 表示解析一个元组。
  • [...]: 表示解析一个列表。

示例:解析复杂参数

int count; double price; char* item_name; PyObject* list_obj; // 接收一个Python列表对象 // 格式字符串 "idsO" 表示:整数,双精度浮点数,字符串,对象 if (!PyArg_ParseTuple(args, "idsO", &count, &price, &item_name, &list_obj)) { return NULL; } // 现在可以安全使用 count, price, item_name, list_obj // 注意:item_name 指向的内存是“借来的”,不要修改它,也不要假设它长期有效。

Py_BuildValue用法类似,方向相反:

// 返回一个Python元组 (result_int, result_str) return Py_BuildValue("(is)", 42, "answer"); // 返回一个Python字典 return Py_BuildValue("{s:i, s:s}", "age", 25, "name", "Bob");

4.2 引用计数:Python内存管理的基石

这是C扩展开发中最核心、也最容易出错的部分。Python使用自动引用计数(ARC)来管理内存。每个Python对象都有一个引用计数,表示有多少个地方引用着它。当计数降为0时,对象会被自动销毁。

在C代码中,你必须手动管理这些引用。主要规则如下:

  1. 谁创建,谁负责:当你调用一个返回新对象引用的API(函数名通常以Py开头,如PyLong_FromLong,Py_BuildValue),你获得的是一个新引用。你有责任在不再需要它时减少其引用计数。
  2. 谁借用,谁不碰:当你通过参数解析(如PyArg_ParseTupleO格式符)或某些API(如PyList_GetItem)获得一个对象时,你得到的是一个借用引用。你不应该减少它的引用计数。它的生命周期由传递给你的调用方管理。
  3. 增加引用:如果你需要长期持有一个借用来的对象,必须调用Py_INCREF(obj)显式增加其引用计数,将其变为一个“拥有”的新引用。
  4. 减少引用:当你不再需要一个“拥有”的新引用时,必须调用Py_DECREF(obj)来减少其引用计数。当计数为0时,对象被销毁。

一个经典的错误示例:

static PyObject* bad_example(PyObject* self, PyObject* args) { PyObject* item; PyObject* my_list = PyList_New(0); // 新引用:my_list 引用计数=1 if (!my_list) return NULL; // 假设 args 是一个包含对象的元组 if (!PyArg_ParseTuple(args, "O", &item)) { // 借用引用:item Py_DECREF(my_list); // 错误发生,需要释放已分配的资源 return NULL; } // 错误!PyList_Append 会“偷走”item的一个引用。 // 如果item是借用引用,这会导致后续不可预知的行为。 // 正确做法:如果需要保留,先 Py_INCREF(item); if (PyList_Append(my_list, item) < 0) { Py_DECREF(my_list); return NULL; } // 此时,my_list 持有 item(引用计数可能已增加) // 但 item 本身作为借用引用,我们不应管理它。 return my_list; // 返回 my_list,将所有权转移给调用者。调用者负责 DECREF。 }

为了避免这些繁琐且易错的操作,对于复杂的扩展,可以考虑使用Cython。Cython是一个将类Python语法编译成C扩展的语言,它能自动处理绝大部分引用计数,极大提升开发效率和安全性。但对于追求极致性能或需要精细控制内存的底层模块,直接使用C API仍是必要的。

5. 性能优化实战:从微基准到算法重构

5.1 性能分析与基准测试

在优化之前,必须先找到瓶颈。不要凭感觉猜测。Python标准库提供了timeit模块,用于对小段代码进行精确计时。对于C扩展,我们可以对比纯Python实现和C扩展实现的性能。

假设我们有一个计算斐波那契数列的函数(这是一个经典的低效递归例子,但很适合做对比)。

纯Python实现 (fib.py):

def fib_py(n): if n <= 1: return n return fib_py(n-1) + fib_py(n-2)

C扩展实现 (fastfib.c):

static long fib_c(long n) { if (n <= 1) return n; return fib_c(n-1) + fib_c(n-2); } static PyObject* fastfib_fib(PyObject* self, PyObject* args) { long n; if (!PyArg_ParseTuple(args, "l", &n)) { return NULL; } long result = fib_c(n); return PyLong_FromLong(result); } // ... 省略模块定义和初始化代码

基准测试脚本:

import timeit import fastfib # 假设编译好的C扩展模块 from fib import fib_py n = 35 # 选择一个适中的数,让计算耗时明显 number = 1 # 只运行一次,因为递归计算很慢 py_time = timeit.timeit(lambda: fib_py(n), number=number) c_time = timeit.timeit(lambda: fastfib.fib(n), number=number) print(f"Python 版本 (n={n}): {py_time:.4f} 秒") print(f"C 扩展版本 (n={n}): {c_time:.4f} 秒") print(f"加速比: {py_time / c_time:.2f}x")

在我的测试环境中(n=35),C扩展版本通常比纯Python版本快20到50倍。这个差距主要来自于Python函数调用的开销、对象创建和解释器循环。对于数值计算密集型任务,C扩展的优势是压倒性的。

5.2 算法级优化与Python对象复用

然而,将递归算法从Python搬到C,虽然消除了语言层面的开销,但算法本身的低效(指数时间复杂度)依然是主要矛盾。真正的性能优化往往发生在算法层面。

优化1:使用迭代法替代递归递归计算斐波那契数列有大量的重复计算。迭代法可以线性时间内完成。

static long fib_iterative(long n) { if (n <= 1) return n; long a = 0, b = 1, temp; for (long i = 2; i <= n; i++) { temp = a + b; a = b; b = temp; } return b; }

这个简单的改动,能将计算fib(35)的时间从数秒降低到微秒级别,性能提升数万倍。这告诉我们,在C扩展中优化算法,比单纯将Python代码翻译成C更重要。

优化2:避免在循环中频繁创建Python对象假设我们要在C扩展中处理一个庞大的Python列表,计算每个元素的平方并返回新列表。 一种低效的做法是在C循环中为每个结果创建Python整数对象并追加到列表:

// 低效版本 PyObject* result_list = PyList_New(PyList_GET_SIZE(input_list)); for (Py_ssize_t i = 0; i < PyList_GET_SIZE(input_list); i++) { PyObject* item = PyList_GetItem(input_list, i); // 借用引用 long val = PyLong_AsLong(item); PyObject* squared = PyLong_FromLong(val * val); // 每次循环都创建新对象 PyList_SET_ITEM(result_list, i, squared); // 设置列表项,会“偷走”squared的引用 }

更高效的做法是,如果结果确定是C原生类型(如long),可以先用C数组计算,最后一次性构建Python列表。

// 高效版本 Py_ssize_t size = PyList_GET_SIZE(input_list); long* temp_array = (long*)malloc(size * sizeof(long)); if (!temp_array) { PyErr_NoMemory(); return NULL; } for (Py_ssize_t i = 0; i < size; i++) { PyObject* item = PyList_GetItem(input_list, i); long val = PyLong_AsLong(item); temp_array[i] = val * val; } PyObject* result_list = PyList_New(size); for (Py_ssize_t i = 0; i < size; i++) { // 注意:PyList_SET_ITEM会“偷走”引用,所以这里直接创建新对象给它。 // 因为temp_array是我们临时用的,没有引用问题。 PyObject* squared = PyLong_FromLong(temp_array[i]); PyList_SET_ITEM(result_list, i, squared); } free(temp_array);

这种方法减少了Python对象分配和垃圾回收的压力,在处理大规模数据时效果显著。

5.3 利用NumPy C API进行大规模数组计算

对于真正的数值计算,直接使用Python列表效率仍然不高。NumPy是科学计算的基石,它提供了ndarray对象和强大的C API。如果你的扩展主要处理大型数值数组,直接集成NumPy C API是最高效的路径。

这允许你在C代码中直接访问NumPy数组底层连续的内存块(对于float64,int32等类型),进行类似纯C数组的操作,完全绕过Python循环。

关键步骤:

  1. setup.py中确保包含NumPy的头文件路径。
    import numpy as np module = Extension(..., include_dirs=[np.get_include()], # 添加这行 ...)
  2. 在C代码中包含<numpy/arrayobject.h>
  3. 在模块初始化函数中调用import_array()宏。
  4. 使用PyArray_*系列函数来检查、转换和访问NumPy数组。

简单示例:对NumPy数组所有元素加1

#include <Python.h> #include <numpy/arrayobject.h> static PyObject* add_one(PyObject* self, PyObject* args) { PyArrayObject* arr = NULL; if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &arr)) return NULL; // 确保是双精度浮点数组,并且是连续内存(C顺序) if (PyArray_TYPE(arr) != NPY_DOUBLE || !PyArray_IS_C_CONTIGUOUS(arr)) { PyErr_SetString(PyExc_TypeError, "需要一个C连续的float64数组"); return NULL; } // 获取指向底层数据的指针和元素数量 double* data = (double*)PyArray_DATA(arr); npy_intp size = PyArray_SIZE(arr); // 直接操作内存 for (npy_intp i = 0; i < size; i++) { data[i] += 1.0; } // 原地修改,返回None。也可以返回原数组的引用。 Py_INCREF(arr); return (PyObject*)arr; }

这种方式实现了真正的“零拷贝”操作,性能与纯C程序无异,是处理海量数据时的终极方案。

6. 高级话题:异常处理、线程安全与调试

6.1 健壮的异常处理

C扩展中发生错误时,不能像C语言那样返回错误码或NULL指针了事,必须通过Python C API设置异常,并让函数返回NULL(对于返回对象的函数)或-1(对于返回整型的函数)。

设置内置异常:

if (some_error_condition) { PyErr_SetString(PyExc_ValueError, "Invalid value provided"); return NULL; } if (memory_allocation_failed) { PyErr_NoMemory(); // 设置内存错误异常 return NULL; } if (index_out_of_range) { PyErr_SetString(PyExc_IndexError, "list index out of range"); return NULL; }

检查Python API调用返回值:许多Python C API函数在失败时会返回NULL-1并自动设置异常。你的代码应该检查这些返回值。

PyObject* new_list = PyList_New(100); if (new_list == NULL) { // PyList_New 失败已经设置了异常(通常是MemoryError) return NULL; // 直接传递错误即可 }

清理资源:在复杂的函数中,如果发生错误,必须确保在返回NULL之前,释放所有已经申请的C资源(如malloc的内存、打开的文件句柄),并减少所有已经增加的Python对象引用计数(Py_DECREF)。

PyObject* result = NULL; char* buffer = malloc(1024); PyObject* temp_obj = PyLong_FromLong(100); if (!buffer || !temp_obj) { PyErr_NoMemory(); goto error; // 使用goto跳转到统一的错误处理段是清晰的做法 } // ... 主要逻辑 result = Py_BuildValue("s", "success"); // 正常退出前释放资源 free(buffer); Py_DECREF(temp_obj); return result; error: // 错误处理段 if (buffer) free(buffer); if (temp_obj) Py_XDECREF(temp_obj); // Py_XDECREF 可以安全地对NULL调用 return NULL;

6.2 线程安全与全局解释器锁(GIL)

Python有一个全局解释器锁(GIL),它确保同一时刻只有一个线程执行Python字节码。这对于简化内存管理至关重要,但也意味着纯Python代码无法实现真正的多核并行。

在C扩展中,规则如下:

  1. 当你的C代码正在操作Python对象(调用Python C API)时,必须持有GIL。从Python调用的C扩展函数,在入口时已经自动获取了GIL。
  2. 如果你的C代码在进行长时间、不涉及Python对象的纯计算(如数值运算、加密、图像处理),可以释放GIL,允许其他Python线程运行。这能显著提升多线程程序的整体吞吐量。

释放与重新获取GIL:

static PyObject* long_running_computation(PyObject* self, PyObject* args) { // ... 解析参数,准备数据(此时持有GIL) // 释放GIL,允许其他线程运行 Py_BEGIN_ALLOW_THREADS // 这里是纯C计算,不调用任何Python C API perform_heavy_calculation(data, data_size); Py_END_ALLOW_THREADS // 重新获取GIL // 将结果包装成Python对象并返回(此时已持有GIL) return Py_BuildValue("i", result); }

警告:在Py_BEGIN_ALLOW_THREADSPy_END_ALLOW_THREADS之间,绝对不要调用任何会与Python解释器交互的API(包括操作PyObject*),否则会导致解释器崩溃或数据损坏。

6.3 调试与内存泄漏排查

调试C扩展比调试纯Python代码更复杂,因为崩溃往往直接导致Python解释器进程中止(段错误)。

1. 使用调试器:在编译扩展时,确保生成调试符号。在setup.py中,可以通过extra_compile_argsextra_link_args传递编译器调试标志。

module = Extension(..., extra_compile_args=['-g', '-O0'], # -g 生成调试信息,-O0 关闭优化 extra_link_args=['-g'], ...)

在Linux/macOS上,可以使用gdblldb来调试:

gdb --args python my_script.py # 在gdb中运行 run,发生崩溃后使用 bt 查看调用栈。

在Windows上,可以使用Visual Studio的调试器附加到Python进程。

2. 使用Valgrind(Linux)检测内存错误:Valgrind是一个强大的内存调试工具,可以检测内存泄漏、非法读写等问题。

valgrind --tool=memcheck --leak-check=full python my_script.py

注意,由于Python解释器自身也会分配大量内存,Valgrind的输出会非常冗长。需要仔细筛选与你扩展模块相关的错误。

3. Python内置工具:

  • sys.getrefcount(obj): 可以在Python层查看对象的引用计数,辅助判断引用管理是否正确。
  • gc模块: 虽然主要管理Python对象的循环引用,但在排查扩展引起的内存异常时也有帮助。

4. 编写详尽的单元测试:这是预防问题最有效的方法。使用Python的unittestpytest框架,为你的C扩展编写覆盖各种边界条件的测试用例。特别是要测试:

  • 各种非法输入(错误类型、空值、越界值)是否触发了正确的异常。
  • 内存管理是否正确,长时间运行是否存在内存缓慢增长(潜在泄漏)。
  • 多线程调用是否安全。

7. 从源码到分发:打包与持续集成

7.1 使用setuptools进行复杂构建

对于简单的扩展,基础的setup.py就够了。但对于依赖第三方C库、需要复杂编译指令的项目,需要更精细的配置。

处理第三方库依赖:假设你的扩展需要链接一个名为libmylib的本地库。

from setuptools import setup, Extension import os # 假设库文件在 /usr/local/lib,头文件在 /usr/local/include lib_dirs = ['/usr/local/lib'] include_dirs = ['/usr/local/include'] # 也可以从环境变量读取 if 'MYLIB_PATH' in os.environ: base_path = os.environ['MYLIB_PATH'] lib_dirs.append(os.path.join(base_path, 'lib')) include_dirs.append(os.path.join(base_path, 'include')) module = Extension('mymodule', sources=['src/mymodule.c'], include_dirs=include_dirs, library_dirs=lib_dirs, libraries=['mylib'], # 要链接的库名 extra_compile_args=['-std=c11', '-Wall', '-Wextra'], # 编译选项 extra_link_args=['-lmylib'], # 链接选项 ) setup( name='mymodule', version='1.0', ext_modules=[module], )

条件编译:有时需要根据平台或Python版本定义不同的宏。可以使用distutilssetuptools的扩展能力。

module = Extension(..., define_macros=[('MY_MODULE_VERSION', '"1.0"'), ('ENABLE_FEATURE_X', '1')], # 定义宏 undef_macros=['DISABLE_FEATURE_Y'], # 取消定义宏 )

在C代码中,就可以使用#ifdef ENABLE_FEATURE_X来进行条件编译。

7.2 制作二进制分发包(Wheel)

为了让用户无需编译环境就能安装你的扩展,你需要制作预编译的二进制分发包,即Wheel文件(.whl)。

这通常需要借助cibuildwheel工具,它可以在CI环境中(如GitHub Actions, Azure Pipelines)为多个平台(Windows, macOS, Linux)和Python版本自动构建Wheel。

一个简化的GitHub Actions工作流示例(.github/workflows/build_wheels.yml):

name: Build wheels on: [push, pull_request] jobs: build_wheels: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install cibuildwheel run: python -m pip install cibuildwheel - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl

本地开发时,你可以使用pip wheel .来为当前平台生成一个wheel文件。

7.3 文档与测试集成

一个专业的扩展项目离不开文档和自动化测试。

文档:除了代码注释,可以使用Sphinx配合autodoc扩展,自动从你的C扩展模块(以及任何配套的Python包装代码)中生成API文档。你需要编写.rst文件来组织内容。

测试集成:setup.py中配置test_suite,可以让用户通过python setup.py test来运行测试。

setup( ..., test_suite='tests', # 指向你的tests目录 )

更现代的做法是使用pytest,并在pyproject.toml中配置:

[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] testpaths = ["tests"]

然后用户可以通过pytest命令直接运行测试。

编写C扩展是一个深入Python底层、挑战与成就感并存的过程。它要求你同时具备C语言的精准控制力和对Python对象模型的深刻理解。从简单的函数封装到复杂的性能优化,每一步都需要仔细权衡。我个人的体会是,不要过早优化,先用Python实现原型,用性能分析工具(如cProfile)定位真正的热点,再考虑用C重写。同时,务必重视测试和错误处理,一个崩溃的C扩展会让整个Python进程宕掉,其破坏力远大于Python代码中的一个异常。当你看到自己编写的扩展模块将关键循环的性能提升数十甚至上百倍时,那种感觉是无与伦比的。最后一个小技巧:在开发初期,可以大量使用PyErr_Print()函数在C代码中打印错误信息到标准错误流,这能帮你快速定位问题所在。

http://www.jsqmd.com/news/1246181/

相关文章:

  • BeeWorks文档中心:企业内网文件管理与协同编辑方案
  • 2026 年至今,钦州性价比高的小区专用声屏障源头厂家怎么联系,住进“静音房”!别让邻居噪音毁了你的生活-永舟丝网 - 行业推荐官【官方】
  • 2026年7月亲身到店探访南通亨得利名表服务中心|全新服务热线及门店地址 - 亨得利官方博客
  • Docker容器化部署实战:从入门到生产环境优化
  • Windows C++程序基址设置与内存管理实践
  • 本人亲测,森优时铁锌维,可以治疗白发
  • Win10下C#免注册调用大漠插件:5步实现自动化开发
  • 课听不懂怎么办对比评测给你方向
  • 深度拆解企业级 Agent 架构:LangGraph + 知识图谱 + 向量检索的协同设计
  • 出生证翻译件在哪里办?如何办?2026三大合规渠道测评
  • 美丽蓝天项目怎么申报?三大核心问题讲透
  • C++图数据结构实现:邻接表与邻接矩阵详解及DFS/BFS遍历
  • Cortex-M4硬故障、MPU与FPU寄存器配置实战指南
  • FineBI数据分析工具安装与部署全指南
  • 计算机毕业设计之基于SpringBoot的社区养老管理系统的设计与实现
  • AI短剧制作全流程解析:从剧本到成片的技术实践
  • 亲身探访郑州亨得利名表服务中心|服务电话与网点地址(2026年7月更新) - 亨得利官方
  • C++自定义配置文件读写:从零实现健壮解析器与工程实践
  • 我可能真的已经具有了黑客的能力----------每个人都能变成黑客
  • 嵌入式第三天
  • 长沙宝珀回收价格查询和靠谱平台实测排行(2026年7月最新) - 收的高名表回收平台
  • CSDN博客-第二篇-Linux基础Day2
  • C++与Eigen实现卡尔曼滤波:从原理到二维轨迹追踪实战
  • Kimi K3 AI编程助手:前端开发效率提升与Claude对比分析
  • 时空数据库与向量检索的融合技术与应用
  • 计算机毕业设计之基于SpringBoot的社区信息化管理系统设计与实现
  • Python逆向网易云音乐加密接口:从AES/RSA原理到歌曲下载实战
  • 大连格拉苏蒂客户服务热线及最新地址信息:2026年7月售后通知 - 亨得利官方服务中心
  • 六西格玛FMEA失效模式分析怎么落地?众智商学院绿带黑带课程教你从理论到实操——400-880-3651张明老师 - 众智商学院cppm官方
  • 萧邦维修中心电话手表维修保养服务热线权威公示(2026年7月最新) - 萧邦中国官方服务中心