Python 进一步扩展
使用 C API 进行 Python 扩展编程
Section titled “使用 C API 进行 Python 扩展编程”Python 的灵活性允许集成用 C 或 C++ 等编译型语言编写的代码。这些可从 Python 调用编译库被称为“扩展”。
Python 扩展模块本质上是一个标准的共享库(在 Unix/Linux 上是 .so,在 Windows 上是 .dll),它暴露了一个与 Python 解释器兼容的接口。
为什么编写 C 扩展?
Section titled “为什么编写 C 扩展?”- 性能:对于 Python 速度不足的计算密集型任务,C/C++ 代码可以显著提高速度。
- 访问 C 库:包装现有的 C/C++ 库,使其在 Python 中可用。
- 低级系统访问:执行标准 Python 模块未直接暴露的操作。
现代替代方案:
Section titled “现代替代方案:”虽然直接的 C API 提供了最大的控制,但有一些现代工具通常可以简化扩展开发:
ctypes:标准库的一部分。无需编写 C 代码即可直接从 Python 调用共享库中的函数,适用于简单接口。cffi:第三方库。允许从 Python 调用 C 代码,对于复杂接口通常比原始 C API 更容易。Cython:一种使 Python C 扩展开发像 Python 本身一样简单的语言。它将类似 Python 的代码(带有可选的 C 类型声明)编译成优化的 C 扩展。Pybind11:一个流行的 C++ 库,用于为 C++ 代码创建 Python 绑定。
本指南重点介绍最低级别的直接 C API 方法。
编写 C 扩展的先决条件
Section titled “编写 C 扩展的先决条件”- C 编译器:你需要一个与你的 Python 安装兼容的 C 编译器(例如,Linux 上的 GCC,Windows 上的 MSVC)。
- Python 开发头文件:这些文件(
Python.h和其他文件)提供了与 Python 解释器交互所需的定义。通过系统的包管理器安装它们(例如,Debian/Ubuntu 上的python3-dev,Fedora/CentOS 上的python3-devel),或者如果你从源代码构建 Python,请确保它们已包含。Windows 安装程序通常会包含它们。 - 构建系统知识:熟悉编译 C 代码和使用构建工具。
此外,假设你对 C 编程和 Python 的对象模型有很好的理解。
简单 C 扩展的结构
Section titled “简单 C 扩展的结构”一个基本的 C 扩展模块通常包括这些部分:
- 包含
Python.h:提供对 Python C API 的访问。 - C 函数:你想暴露给 Python 的函数的实际实现。
- 方法定义表:将 Python 函数名映射到它们的 C 实现。
- 模块定义结构:描述模块本身(名称、文档字符串、方法)。
- 初始化函数:Python 导入模块时调用的入口点。
1. 包含 Python.h
Section titled “1. 包含 Python.h”此头文件必须首先包含在你的 C 源文件中。
#include <Python.h>2. C 函数(实现扩展逻辑)
Section titled “2. C 函数(实现扩展逻辑)”这些 C 函数将从 Python 调用。它们通常接受 PyObject* 参数并返回 PyObject*。PyObject 是表示任何 Python 对象的基本 C 结构体。
常见签名:
// 接受可变参数的函数(例如 *args)static PyObject * my_c_function(PyObject *self, PyObject *args);
// 接受可变参数和关键字参数的函数(例如 **kwargs)static PyObject * my_c_function_kw(PyObject *self, PyObject *args, PyObject *kwargs);
// 不接受参数的函数static PyObject * my_c_function_noargs(PyObject *self);self 通常指代模块级别的函数所对应的模块对象。args 是位置参数的元组。kwargs 是关键字参数的字典。
函数必须返回一个 PyObject*。要返回 Python 的 None,请使用 Py_RETURN_NONE。如果发生错误,请在设置适当的 Python 异常后返回 NULL。
按照惯例,除非需要外部使用,C 函数应声明为 static。命名通常结合模块名和函数名(例如 spam_system)。
示例 C 函数:
// 示例 C 函数实现(详情稍后)static PyObject * spam_system(PyObject *self, PyObject *args) { const char *command; int sts;
// 从 Python 元组 'args' 解析参数 if (!PyArg_ParseTuple(args, "s", &command)) { // PyArg_ParseTuple 在失败时引发 TypeError return NULL; // 指示错误 }
// 调用 C 库函数 sts = system(command);
// 检查 C 错误并在需要时引发 Python 异常 if (sts < 0) { PyErr_SetString(PyExc_OSError, "system() failed"); // system() 调用失败 return NULL; }
// 构建并返回一个 Python 整数 return PyLong_FromLong(sts);}3. 方法定义表 (PyMethodDef)
Section titled “3. 方法定义表 (PyMethodDef)”此数组映射 Python 函数名、C 函数指针、参数类型和文档字符串。
结构:
struct PyMethodDef { const char *ml_name; /* Python 函数名 */ PyCFunction ml_meth; /* C 函数指针 */ int ml_flags; /* 参数标志 (METH_VARARGS, METH_KEYWORDS, METH_NOARGS) */ const char *ml_doc; /* 文档字符串 */};标志确定预期的 C 函数签名:
METH_VARARGS:函数预期接受(PyObject *self, PyObject *args)。METH_KEYWORDS:函数预期接受(PyObject *self, PyObject *args, PyObject *kwargs)。可以与METH_VARARGS进行或运算。METH_NOARGS:函数预期接受(PyObject *self)。不允许从 Python 传递参数。
该表必须以哨兵条目 {NULL, NULL, 0, NULL} 结束。
示例表:
static PyMethodDef SpamMethods[] = { // {Python 名称, C 函数, 标志, 文档字符串} {"system", spam_system, METH_VARARGS, "执行一个 shell 命令。"}, // 在此处添加其他函数... {NULL, NULL, 0, NULL} /* 哨兵 */};4. 模块定义结构 (PyModuleDef)
Section titled “4. 模块定义结构 (PyModuleDef)”此结构包含模块本身的所有信息。
结构:
static struct PyModuleDef spammodule = { PyModuleDef_HEAD_INIT, "spam", /* 模块名 */ "Example module that provides a function.", /* 模块文档字符串 */ -1, /* 每个解释器状态的大小,-1 表示没有状态 */ SpamMethods /* 上面定义的方法表 */ /* 其他字段如 m_slots, m_traverse, m_clear, m_free 可以为 NULL */};5. 初始化函数 (PyInit_modulename)
Section titled “5. 初始化函数 (PyInit_modulename)”这是唯一一个非 static 的函数。Python 在首次导入模块时调用它。它的名称必须是 PyInit_ 后跟 PyModuleDef 中指定的模块名。
它使用 PyModule_Create() 和模块定义结构。
语法:
PyMODINIT_FUNC // 确保正确可见性和返回类型的宏PyInit_spam(void) { return PyModule_Create(&spammodule);}完整简单示例 (spam.c)
Section titled “完整简单示例 (spam.c)”#define PY_SSIZE_T_CLEAN // Recommended for modern C API usage#include <Python.h>#include <stdlib.h> // 用于 system()
// 1. C 函数实现static PyObject * spam_system(PyObject *self, PyObject *args) { const char *command; int sts;
if (!PyArg_ParseTuple(args, "s", &command)) { return NULL; } sts = system(command); if (sts < 0) { PyErr_SetString(PyExc_OSError, "system() failed"); // system() 调用失败 return NULL; } return PyLong_FromLong(sts);}
// 2. 方法定义表static PyMethodDef SpamMethods[] = { {"system", spam_system, METH_VARARGS, "执行一个 shell 命令。"}, {NULL, NULL, 0, NULL} /* 哨兵 */};
// 3. 模块定义结构static struct PyModuleDef spammodule = { PyModuleDef_HEAD_INIT, "spam", /* 模块名 */ "Example module documentation.", /* 模块文档字符串,可以为 NULL */ -1, /* 每个解释器状态的大小,或者 -1 如果模块在全局变量中保留状态 */ SpamMethods};
// 4. 初始化函数PyMODINIT_FUNC PyInit_spam(void) { return PyModule_Create(&spammodule);}构建和安装扩展
Section titled “构建和安装扩展”Python 的标准构建工具(setuptools、build)用于编译和安装扩展。distutils 基本已废弃。
你通常需要一个 setup.py 文件(或者带有构建后端配置的 pyproject.toml)。
示例 setup.py(使用 setuptools):
Section titled “示例 setup.py(使用 setuptools):”from setuptools import setup, Extension
spam_module = Extension('spam', # Python 模块名 sources=['spam.c']) # 源文件列表
setup( name='spam', # 包名 version='1.0', # 版本 description='示例 C 扩展模块', ext_modules=[spam_module] # Extension 对象列表)构建/安装命令:
Section titled “构建/安装命令:”# 推荐的现代方式(需要 'build' 包:pip install build)# 在 'dist/' 目录中构建 wheel 和 sdistpython -m build
# 安装构建好的 wheel(替换为实际的 wheel 文件名)pip install dist/spam-1.0-cp310-cp310-linux_x86_64.whl
# --- 或者 ---(旧的 setuptools 直接安装方式)# 直接编译并安装到当前 Python 环境中# 根据环境,可能需要 root/admin 权限# python setup.py install导入和使用扩展
Section titled “导入和使用扩展”安装后,像使用任何其他 Python 模块一样导入和使用它:
#!/usr/bin/env python3
import spamimport os
print("文档字符串:", spam.__doc__)
# 示例:使用扩展列出目录内容try: # 使用一个安全的命令进行演示 command = f"ls -l {os.getcwd()}" # 使用适合你操作系统的命令 status = spam.system(command) print(f"\n命令退出状态: {status}") # \n命令退出状态: {status}except OSError as e: print(f"执行命令错误: {e}") # 执行命令错误: {e}except Exception as e: print(f"发生了一个意外错误: {e}") # 发生了一个意外错误: {e}传递参数 (PyArg_ParseTuple)
Section titled “传递参数 (PyArg_ParseTuple)”PyArg_ParseTuple() 函数(及相关的 PyArg_ParseTupleAndKeywords())在你的 C 函数内部用于提取从 Python 传递的参数。
int PyArg_ParseTuple(PyObject *args, const char *format, ...);args:包含位置参数的 PyObject* 元组。
format:一个格式字符串,指定预期的参数类型。
...:指向 C 变量的指针,解析后的值将存储在这些变量中。
成功时返回 true,失败时返回 false(并设置一个 Python 异常)。
常见格式码:
Section titled “常见格式码:”| Code | Type | 描述 |
|---|---|---|
s | string | Python 字符串 -> const char*(假定为 UTF-8)。使用 es 或 et 进行显式编码控制。 |
z | string or None | const char* 或 NULL -> Python 字符串或 None。 |
i | integer | Python int -> C int。 |
l | integer | Python int -> C long。 |
L | integer | Python int -> C long long。 |
d | float | Python float -> C double。 |
f | float | Python float -> C float。 |
O | object | 传递对象,增加引用计数(由调用者拥有)。使用 O! 进行类型检查,使用 O& 进行自定义转换器。 |
N | object | 传递对象,窃取引用(调用者失去所有权)。 |
(items) | tuple | 需要序列,并根据包含的格式解析各项。 |
| | 指示后续参数是可选的。 | |
: | 后跟函数名,用于错误消息。 | |
; | 后跟完整错误消息。 |
示例解析多个参数:
static PyObject * my_func(PyObject *self, PyObject *args) { int i_val; double d_val; const char *s_val; PyObject *o_val;
// 预期接收一个 int, 一个 double, 一个 string 和任意一个对象 if (!PyArg_ParseTuple(args, "idsO", &i_val, &d_val, &s_val, &o_val)) { return NULL; // 错误:PyArg_ParseTuple 设置异常 }
printf("已解析:int=%d, double=%f, string='%s'\n", i_val, d_val, s_val); // 使用 PyObject_Print 或其他 API 函数处理 o_val
Py_RETURN_NONE; // 成功时返回 None}返回值 (Py_BuildValue)
Section titled “返回值 (Py_BuildValue)”要将 C 函数的值返回给 Python,可以使用 PyLong_FromLong、PyFloat_FromDouble、PyUnicode_FromString 等函数,或者多功能的 Py_BuildValue() 来构建 PyObject*。
Py_BuildValue() 使用类似于 PyArg_ParseTuple 的格式字符串,但接受 C 值作为输入。
PyObject* Py_BuildValue(const char *format, ...);返回指向创建的 Python 对象的新引用,错误时返回 NULL。
常见格式码:
Section titled “常见格式码:”| Code | C Type | 描述 |
|---|---|---|
s | const char* | C 字符串 -> Python 字符串(假定为 UTF-8)。 |
z | const char* or NULL | C 字符串或 NULL -> Python 字符串或 None。 |
i | int | C int -> Python int。 |
l | long | C long -> Python int。 |
L | long long | C long long -> Python int。 |
d | double | C double -> Python float。 |
f | float | C float -> Python float。 |
O | PyObject* | 传递对象,增加引用计数(由调用者拥有)。 |
N | PyObject* | 传递对象,窃取引用(调用者失去所有权)。 |
(items) | C values… | 从 C 值构建一个 Python 元组。 |
[items] | C values… | 构建一个 Python 列表。 |
{items} | key, value, … | 构建一个 Python 字典(键1,值1,键2,值2…)。 |
示例返回值:
static PyObject * foo_add_subtract(PyObject *self, PyObject *args) { int a, b; if (!PyArg_ParseTuple(args, "ii", &a, &b)) { return NULL; } // 返回一个元组 (a + b, a - b) return Py_BuildValue("(ii)", a + b, a - b);}Python 使用引用计数进行内存管理。在 C 扩展中,你必须使用 Py_INCREF(obj) 增加引用计数,使用 Py_DECREF(obj) 减少引用计数,正确管理引用计数。未能正确管理将导致内存泄漏或程序崩溃。
PyLong_FromLong 和 Py_BuildValue 等函数返回新引用(调用者拥有它们)。带有 O 格式码的 PyArg_ParseTuple 返回借用引用(除非你先对其调用 incref,否则不要调用 decref)。返回对象时,所有权通常会传递给调用者。
小心管理引用是编写 C 扩展中最复杂的方面之一。像 Cython 这样的工具会自动处理这个问题。