Skip to content

Python 进一步扩展

Python 的灵活性允许集成用 C 或 C++ 等编译型语言编写的代码。这些可从 Python 调用编译库被称为“扩展”。

Python 扩展模块本质上是一个标准的共享库(在 Unix/Linux 上是 .so,在 Windows 上是 .dll),它暴露了一个与 Python 解释器兼容的接口。

  • 性能:对于 Python 速度不足的计算密集型任务,C/C++ 代码可以显著提高速度。
  • 访问 C 库:包装现有的 C/C++ 库,使其在 Python 中可用。
  • 低级系统访问:执行标准 Python 模块未直接暴露的操作。

虽然直接的 C API 提供了最大的控制,但有一些现代工具通常可以简化扩展开发:

  • ctypes:标准库的一部分。无需编写 C 代码即可直接从 Python 调用共享库中的函数,适用于简单接口。
  • cffi:第三方库。允许从 Python 调用 C 代码,对于复杂接口通常比原始 C API 更容易。
  • Cython:一种使 Python C 扩展开发像 Python 本身一样简单的语言。它将类似 Python 的代码(带有可选的 C 类型声明)编译成优化的 C 扩展。
  • Pybind11:一个流行的 C++ 库,用于为 C++ 代码创建 Python 绑定。

本指南重点介绍最低级别的直接 C API 方法。

  • 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 扩展模块通常包括这些部分:

  • 包含 Python.h:提供对 Python C API 的访问。
  • C 函数:你想暴露给 Python 的函数的实际实现。
  • 方法定义表:将 Python 函数名映射到它们的 C 实现。
  • 模块定义结构:描述模块本身(名称、文档字符串、方法)。
  • 初始化函数:Python 导入模块时调用的入口点。

此头文件必须首先包含在你的 C 源文件中。

#include <Python.h>

这些 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);
}

此数组映射 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} /* 哨兵 */
};

此结构包含模块本身的所有信息。

结构:

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 */
};

这是唯一一个非 static 的函数。Python 在首次导入模块时调用它。它的名称必须是 PyInit_ 后跟 PyModuleDef 中指定的模块名。

它使用 PyModule_Create() 和模块定义结构。

语法:

PyMODINIT_FUNC // 确保正确可见性和返回类型的宏
PyInit_spam(void) {
return PyModule_Create(&spammodule);
}
#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);
}

Python 的标准构建工具(setuptools、build)用于编译和安装扩展。distutils 基本已废弃。

你通常需要一个 setup.py 文件(或者带有构建后端配置的 pyproject.toml)。

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 对象列表
)
# 推荐的现代方式(需要 'build' 包:pip install build)
# 在 'dist/' 目录中构建 wheel 和 sdist
python -m build
# 安装构建好的 wheel(替换为实际的 wheel 文件名)
pip install dist/spam-1.0-cp310-cp310-linux_x86_64.whl
# --- 或者 ---(旧的 setuptools 直接安装方式)
# 直接编译并安装到当前 Python 环境中
# 根据环境,可能需要 root/admin 权限
# python setup.py install

安装后,像使用任何其他 Python 模块一样导入和使用它:

#!/usr/bin/env python3
import spam
import 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() 函数(及相关的 PyArg_ParseTupleAndKeywords())在你的 C 函数内部用于提取从 Python 传递的参数。

int PyArg_ParseTuple(PyObject *args, const char *format, ...);

args:包含位置参数的 PyObject* 元组。

format:一个格式字符串,指定预期的参数类型。

...:指向 C 变量的指针,解析后的值将存储在这些变量中。

成功时返回 true,失败时返回 false(并设置一个 Python 异常)。

CodeType描述
sstringPython 字符串 -> const char*(假定为 UTF-8)。使用 es 或 et 进行显式编码控制。
zstring or Noneconst char* 或 NULL -> Python 字符串或 None。
iintegerPython int -> C int。
lintegerPython int -> C long。
LintegerPython int -> C long long。
dfloatPython float -> C double。
ffloatPython float -> C float。
Oobject传递对象,增加引用计数(由调用者拥有)。使用 O! 进行类型检查,使用 O& 进行自定义转换器。
Nobject传递对象,窃取引用(调用者失去所有权)。
(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
}

要将 C 函数的值返回给 Python,可以使用 PyLong_FromLong、PyFloat_FromDouble、PyUnicode_FromString 等函数,或者多功能的 Py_BuildValue() 来构建 PyObject*。

Py_BuildValue() 使用类似于 PyArg_ParseTuple 的格式字符串,但接受 C 值作为输入。

PyObject* Py_BuildValue(const char *format, ...);

返回指向创建的 Python 对象的新引用,错误时返回 NULL。

CodeC Type描述
sconst char*C 字符串 -> Python 字符串(假定为 UTF-8)。
zconst char* or NULLC 字符串或 NULL -> Python 字符串或 None。
iintC int -> Python int。
llongC long -> Python int。
Llong longC long long -> Python int。
ddoubleC double -> Python float。
ffloatC float -> Python float。
OPyObject*传递对象,增加引用计数(由调用者拥有)。
NPyObject*传递对象,窃取引用(调用者失去所有权)。
(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 这样的工具会自动处理这个问题。