使用 Python API 实现 C++ 和 Python 的交互





5.00/5 (11投票s)
本文解释了 Python API 如何实现在 C++ 中嵌入 Python,以及如何用 C++ 编写可在 Python 中导入的扩展模块。
1. 引言
根据 StackOverflow,C++ 和 Python 是桌面开发最流行的两种编程语言。许多应用程序为了性能用 C++ 编写,并提供一个 Python 接口以实现配置和脚本功能。不幸的是,这两种语言差异太大,许多开发者不知道如何在 C++ 中访问 Python 代码,或者从 Python 中调用 C++ 函数。
幸运的是,Python 的一些实现(例如 python.org 的参考实现)提供了 C/C++ 头文件和库,简化了 C++ 和 Python 接口的过程。这些头文件和库构成了 Python API,本文的目标是解释如何使用 Python。
具体来说,本文重点介绍使用 Python API 的两种方式。文章的第一部分解释了如何在 C++ 代码中访问 Python。这称为 嵌入 Python。第二部分解释了如何编写一个可以作为 Python 模块访问的 C++ 库。这称为 扩展模块。
但在讨论这两个主题之前,我需要解释如何下载 Python 并将其安装到你的系统上。如果你已经熟悉 Python,请随意跳过下一节。
2. 安装 Python
Python 实现 是一套工具,提供 Python 解释器、基本 Python 模块和其他实用程序(如 pip)。根据我的经验,Python 有四种主要实现
- CPython - 最古老、最流行,用 C 编写
- PyPy - 类似于 CPython,但使用即时编译来提高性能
- Jython - 用 Java 编写,将 Python 转换为字节码
- IronPython - 用 C# 编写,使 Python 能够访问 C# 和 .NET 功能
CPython 是唯一提供 C/C++ 接口的 Python 实现,因此本文重点介绍 CPython。如果你运行的是 Linux,可以使用包管理器安装它(在 Ubuntu 上使用 apt-get install python3.11 python3-dev
,在 RHEL 和 CentOS 上使用 yum install python3 python3-devel
)。
如果你运行的是 Windows 或 macOS,可以从 Python 下载网站 下载安装程序。选择你的操作系统,然后点击最新版本的 Python 链接,你的浏览器将下载可执行文件。运行可执行文件时,会弹出一个对话框询问配置设置。下图显示了 Windows 3.11.4 版本的情况。
在对话框底部,勾选 将 python.exe 添加到 PATH 复选框。这确保你能够从命令行启动 Python 解释器。
如果你在 Windows 上点击 立即安装,Python 将安装在 AppData\Local\Programs 文件夹中。你可以通过点击 自定义安装 链接并选择不同的文件夹来自定义。安装完成后,点击对话框的 关闭 按钮。
对于本文而言,Python 安装在哪里并不重要,但知道安装目录很重要。如果你查看顶层 include 目录,你会找到 Python API 的头文件。libs 目录包含链接 C/C++ 应用程序所需的库。在我的 Windows 系统上,所需的库文件名为 python311.lib。在我的 Linux 系统上,它的名称是 libpython3.11.so。
3. 在 C/C++ 中嵌入 Python
Python API 提供了几个头文件,声明了能够访问 Python 模块和代码的 C/C++ 函数。在外部代码中访问 Python 的技术术语称为 嵌入,核心头文件是 Python.h。
本节的目标是查看 Python.h 中可以实现嵌入的函数。它们使用起来可能会令人沮丧,因为 Python 数据结构都由 PyObject
数据类型的实例表示。模块由 PyObject
s 表示,函数和方法由 PyObject
s 表示,变量也由 PyObject
s 表示。
本文将不讨论 Python API 中的所有函数,甚至大多数函数。相反,我们将这些函数分为两类
- 基本函数 - 访问模块、方法和属性的函数
- 对象创建和转换 - 创建
PyObject
s 并将其转换为其他类型的函数
在探讨这些函数之后,本节将介绍读取简单 Python 模块中的函数、设置其参数,然后执行该 Python 函数的 C++ 代码。
3.1 基本函数
要在 C++ 应用程序中嵌入 Python 处理,开发者应该熟悉一组核心函数。表 1 列出了它们并提供了每个函数的描述。
表 1:Python API 的基本函数
函数签名 | 描述 |
Py_Initialize() | 初始化解释器和模块 |
Py_Finalize() | 释放解释器和资源 |
PyImport_ImportModule(const char*) | 导入给定模块 |
PyObject_HasAttrString( PyObject*, const char*) | 检查属性是否存在 |
PyObject_GetAttrString( PyObject*, const char*) | 访问给定属性 |
PyCallable_Check(PyObject*) | 检查属性是否可执行 |
PyObject_Repr(PyObject*) | 从打印表示创建 PyObject |
PyObject_Str(PyObject*) | 从字符串表示创建 PyObject |
PyObject_CallObject( PyObject*, PyObject*) | 用参数执行对象 |
PyINCREF(PyObject*) | 增加引用计数(不能为 null ) |
PyXINCREF(PyObject*) | 增加引用计数(可以为 null ) |
PyDECREF(PyObject*) | 减少引用计数(不能为 null ) |
PyXDECREF(PyObject*) | 减少引用计数(可以为 null ) |
第一个函数 Py_Initialize
尤为重要,因为它执行使 Python 在 C/C++ 中可用的任务。在应用程序访问 Python 模块和功能之前,必须调用此函数。
初始化环境后,应用程序可以通过调用 PyImport_ImportModule
访问 Python 模块。此函数接受模块名称并返回表示该模块的 PyObject
指针。如果模块包含在 Python 文件中,则应省略 *.py 后缀。
例如,以下函数调用访问 simple.py 中的代码
PyObject *mod = PyImport_ImportModule("simple");
一旦应用程序访问了模块或数据结构,它就可以检查其属性。PyObject_HasAttrString
函数识别属性是否存在。如果属性存在,PyObject_GetAttrString
返回表示该属性的 PyObject
指针。
例如,以下代码从 simple.py 访问名为 plus
的属性。
PyObject *mod, *attr;
mod = PyImport_ImportModule("simple");
if (mod != nullptr) {
if (PyObject_HasAttrString(mod, "plus") == 1) {
attr = PyObject_GetAttrString(mod, "plus");
}
}
属性和函数都作为属性访问,但函数可以调用而属性不能。要检查属性是否可以调用,应用程序需要调用 PyCallable_Check
,如果属性可以调用,它返回 1
,否则返回 0
。
如果属性可以调用,PyObject_CallObject
告诉解释器执行该属性。第一个参数是表示该属性的 PyObject
指针,第二个参数是表示包含方法参数的元组的 PyObject
指针。
每个 PyObject
都有一个引用计数,用于标识它被访问的次数。创建时,计数设置为 1
。应用程序可以通过调用 PyINCREF
或 PyXINCREF
来增加引用计数。两者都接受指向 PyObject
的指针,第一个函数只有在指针不为 NULL
时才应调用。第二个函数 PyXINCREF
可以在指针为 null
时调用。
当不再需要 PyObject
时,应用程序应调用 PyDECREF
或 PyXDECREF
来减少对象的引用计数。一旦计数达到 0
,对象将被释放。这两个函数都接受指向 PyObject
的指针,并且只有在指针不为 null 时才应调用 PyDECREF
。
3.2 对象创建和转换
在许多情况下,应用程序需要从常规数据创建 PyObject
,或从 PyObject
提取常规数据。应用程序还可能需要创建 Python 特定的结构,如列表或元组。表 2 列出了执行这些任务的函数。
表 2:对象创建/转换函数
函数签名 | 描述 |
PyLong_FromLong(long) | 从长整型创建 PyObject |
PyLong_AsLong(PyObject*) | 返回 PyObject 的长整型 |
PyFloat_FromDouble(double) | 从双精度浮点型创建 PyObject |
PyFloat_AsDouble(PyObject*) | 返回 PyObject 的双精度浮点型 |
PyUnicode_FromString(const char*) | 从字符串创建 PyObject |
PyUnicode_AsEncodedString( PyObject*, const char*, const char*) | 从编码字符串创建 PyObject |
PyBytes_FromString(const char*) | 从字符串创建 PyObject |
PyBytes_AsString(PyObject*) | 返回 PyObject 的字符串 |
PyTuple_New(Py_ssize_t) | 创建表示元组的 PyObject |
PyTuple_GetItem(PyObject*, Py_ssize_t) | 返回元组的给定元素 |
PyTuple_SetItem(PyObject*, Py_ssize_t, PyObject*) | 设置元组的给定元素 |
PyList_New(Py_ssize_t) | 创建表示列表的 PyObject |
PyList_GetItem(PyObject*, Py_ssize_t) | 返回列表的给定元素 |
PyList_SetItem(PyObject*, Py_ssize_t, PyObject*) | 设置列表的给定元素 |
当应用程序需要读取或设置属性值时,这些函数变得很重要。例如,如果 float_attr
是包含浮点值的 Python 属性,PyFloat_AsDouble
将返回一个可以在 C/C++ 中处理的 double
。
处理文本很复杂。Python API 可以通过调用 PyUnicode_FromString
从 string
创建 Unicode PyObject
。你还可以通过调用 PyBytes_FromString
从 string
创建字节 PyObject
。
显示属性的 string
也比较复杂。PyObject_Str
返回包含对象 string
的 PyObject
,PyUnicode_AsEncodedString
将其转换为包含 string
编码表示的字节 PyObject
。然后 PyBytes_AsString
返回与编码 string
对应的 C/C++ string
。
例如,以下代码获取 my_attr
属性的 string
表示,使用 UTF-8 进行编码,并打印相应的 C/C++ string
。
PyObject* attr = PyObject_GetAttrString(mod, "my_attr");
PyObject* str = PyObject_Str(attr);
PyObject* ucode = PyUnicode_AsEncodedString(str, "utf-8", NULL);
const char* bytes = PyBytes_AsString(ucode);
std::cout << bytes << std::endl;
要将参数传递给 Python 方法,应用程序需要创建一个元组,并为每个要传递的参数插入一个元素。在代码中,可以通过调用 PyTuple_New
创建元组,并通过调用 PyTuple_SetItem
设置元素。类似地,应用程序可以通过调用 PyList_New
创建 Python 列表,并通过调用 PyList_SetItem
设置其元素。
3.3 简单嵌入示例
为了演示嵌入的工作原理,本文的源代码包含两个源文件
- simple.py - 一个简单的 Python 文件,定义了一个名为
plus
的函数,它返回其两个参数的和 - embedding.cpp - 一个 C++ 应用程序,使用 Python API 访问 simple.py 中的代码
以下代码展示了 embedding.cpp 的内容。它访问 simple.py,找到 plus
属性,设置其参数,并执行该函数。
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <iostream>
int main() {
PyObject *module, *func, *args, *ret;
// Initialize python access
Py_Initialize();
// Import the simple.py code
module = PyImport_ImportModule("simple");
if (module) {
// Access the attribute named plus
func = PyObject_GetAttrString(module, "plus");
// Make sure the attribute is callable
if (func && PyCallable_Check(func)) {
// Create a tuple to contain the function's args
args = PyTuple_New(2);
PyTuple_SetItem(args, 0, PyLong_FromLong(4));
PyTuple_SetItem(args, 1, PyLong_FromLong(7));
// Execute the plus function in simple.py
ret = PyObject_CallObject(func, args);
Py_DECREF(args);
Py_DECREF(func);
Py_DECREF(module);
// Check the return value
if (ret) {
// Convert the value to long and print
long retVal = PyLong_AsLong(ret);
std::cout << "Result: " << retVal << std::endl;
}
else {
// Display error
PyErr_Print();
std::cerr << "Couldn't access return value" << std::endl;
Py_Finalize();
return 1;
}
}
else {
// Display error
if (PyErr_Occurred())
PyErr_Print();
std::cerr << "Couldn't execute function" << std::endl;
}
}
else {
// Display error
PyErr_Print();
std::cerr << "Couldn't access module" << std::endl;
Py_Finalize();
return 1;
}
// Finalize the Python embedding
Py_Finalize();
return 0;
}
访问模块后,应用程序通过调用 PyObject_CallObject
调用模块的 plus
函数。它传递两个参数:表示函数的属性和包含要传递给 plus
函数的两个值的元组。以下代码显示了 simple.py 中 plus
函数的样子。
def plus(a, b):
return a + b
要从 embedding.cpp 构建应用程序,你需要告诉编译器 Python 安装目录的 include 文件夹中的头文件和 libs 文件夹中的库文件。当我在我的系统上运行应用程序时,输出如下
Result: 11
4. 创建 Python 扩展
Python 解释器可以访问包括 os
、datetime
和 string
在内的内置模块。使用 Python API,我们可以添加名为扩展模块的新内置模块。与常规模块不同,扩展模块是用 C 或 C++ 编写的动态库。
本讨论将逐步讲解一个名为 plustwo
的扩展模块的开发。它包含一个名为 addtwo
的函数,该函数接受一个数字并返回该数字加 2 的和。
python
>>> import plustwo
>>> x = plustwo.addtwo(5)
>>> x
7
编程扩展模块很难,因为函数需要有特殊的名称,并且必须提供特殊的数据结构。要理解这个过程,你需要注意三点
- 如果所需的模块是
modname
,代码必须定义一个名为PyInit_modname
且不接受任何参数的函数。对于示例,模块名为plustwo
,因此代码定义了一个名为PyInit_plustwo
的函数。 - 为了描述模块,代码必须创建一个
PyModuleDef
结构并设置其字段。这些字段标识模块的名称、其文档和其方法。 - 对于模块中的每个函数,代码必须创建一个
PyMethodDef
结构并设置其字段。这些字段标识方法的名称、参数和文档。它还必须标识在调用方法时将调用的函数。
本节详细讨论这些要点,然后展示如何在代码中实现 plustwo
扩展模块。
4.1 PyInit 函数
当 Python 解释器首次导入扩展模块 modname
时,它将调用 PyInit_modname
函数。必须正确编写此函数,以确保解释器可以执行它,并且需要遵循五个规则
- 它必须以
PyMODINIT_FUNC
宏为前缀,并且必须是唯一以该宏为前缀的函数。 - 它不能是
static
,并且必须是代码中唯一的非static
项。 - 它不能接受任何参数。
- 它必须使用对描述模块的
PyModuleDef
的引用调用PyModuleCreate
。 - 其返回值必须设置为
PyModuleCreate
的返回值。
理解这些规则的最佳方法是看一个例子。如果模块名称为 plustwo
,并且描述模块的 PyModuleDef
结构为 moduleDef
,则以下代码显示了如何编写 PyInit_plustwo
PyMODINIT_FUNC PyInit_plustwo() {
return PyModule_Create(&moduleDef);
}
至少,函数需要调用 PyModule_Create
并带上 PyModuleDef
引用并返回结果。但函数可以编写得不仅仅是调用 PyModule_Create
。
4.2 PyModuleDef 结构
扩展模块需要创建一个 PyModuleDef
结构,以告诉 Python 解释器如何处理该模块。表 3 列出了此结构的每个字段及其数据类型。
表 3:PyModuleDef 结构的字段
字段名 | 数据类型 | 描述 |
m_base | PyModuleDef_Base | 始终设置为 PyModuleDef_HEAD_INIT |
m_name | const char* | 模块名称 |
m_doc | const char* | 模块描述 |
m_size | Py_ssize_t | 用于存储模块状态的内存大小 |
m_methods | PyMethodDef* | 方法描述符数组 |
m_slots | PyMethodDef_Slot* | 方法槽数组 |
m_traverse | traverseproc | 遍历函数 |
m_clear | inquiry | 查询函数 |
m_free | freefunc | 释放资源的函数 |
本文重点关注前五个字段,第一个字段应始终设置为 PyModuleDef_HEAD_INIT
。第二个字段应设置为模块的名称,第三个字段应设置为模块的 docstring
,当调用 help
函数时会显示该 docstring。
第四个字段对于需要多阶段初始化和子解释器的模块很重要。此字段标识应预留多少内存来存储模块的状态数据。对于大多数扩展模块来说,这不是问题,因此 m_size
应设置为 -1
。这在以下代码中显示
static struct PyModuleDef moduleDef = {
PyModuleDef_HEAD_INIT,
"plustwo",
"This module contains a function (addtwo) that adds two to a number\n",
-1,
funcs // Array containing a PyMethodDef for each module function
};
m_methods
字段必须设置为一个数组,该数组包含模块中每个函数的一个 PyMethodDef
结构。在示例中,plustwo
模块有一个名为 addtwo
的函数。因此,示例代码中的 funcs
数组包含一个 PyMethodDef
结构。
4.3 PyMethodDef 结构
扩展模块通过提供一个 PyMethodDef
结构数组来标识其函数。PyMethodDef
的字段标识函数的名称、参数和 docstring。表 4 列出了这些字段及其数据类型。
表 4:PyMethodDef 结构的字段
字段名 | 数据类型 | 描述 |
ml_name | const char* | 函数名称 |
ml_meth | PyCFunction | 提供代码的 C 函数 |
ml_flags | int | 标识函数参数的标志 |
ml_doc | const char* | 函数的 docstring |
第二个字段 ml_meth
必须设置为一个 C 函数,该函数提供在调用模块函数时要执行的代码。编写此函数时,需要记住四个规则
- 如果模块函数与模块名称不同,则 C 函数的名称应设置为
modname_funcname
,其中modname
是模块的名称,funcname
是函数的名称。 - 如果模块函数与它所在的模块名称相同,则 C 函数的名称应设置为
modname
。 - 函数接受的参数数量由
PyMethodDef
的ml_flags
参数决定。 - C 函数必须声明为
static
,并且必须返回一个PyObject
指针。如果函数不返回任何值,它应该使用Py_RETURN_NONE
返回一个空对象。
第三个字段 ml_flags
标识函数接受的参数的性质。这通常设置为以下三个值之一
METH_NOARGS
- 函数接受一个表示模块的PyObject*
参数。METH_O
- 函数接受两个参数:一个表示模块的PyObject*
和一个表示单个参数的PyObject*
。METH_VARARGS
- 函数接受两个参数:一个表示模块的PyObject*
和一个表示包含函数参数的元组的PyObject*
。
对于本文的示例,addtwo
函数接受一个数字参数,并返回该参数与 2 的和。因为只有一个参数,所以 ml_flags
应该设置为 METH_O
。
4.4 plustwo 扩展模块
至此,你应该对扩展模块中必须创建的函数和数据结构有了基本的了解。以下列表显示了 plustwo.cpp 中的代码,它是本文源代码的一部分。此文件定义了一个名为 plustwo
的扩展模块,其中包含一个名为 addtwo
的函数
#define PY_SSIZE_T_CLEAN
#include <Python.h>
// The code to be executed when the module function is called
static PyObject* plustwo_addtwo(PyObject* self, PyObject* arg) {
long longArg = PyLong_AsLong(arg) + 2;
return PyLong_FromLong(longArg);
}
// Array of PyMethodDef structures - describe the module's functions
static PyMethodDef funcs[] = {
{"addtwo", (PyCFunction)plustwo_addtwo, METH_O, "This adds two to a number\n"},
{NULL, NULL, 0, NULL}
};
// The PyModuleDef structure describes the module
static struct PyModuleDef moduleDef = {
PyModuleDef_HEAD_INIT,
"plustwo",
"This module contains a function (addtwo) that adds two to a number\n",
-1,
funcs
};
// Called when the interpreter imports the module
PyMODINIT_FUNC PyInit_plustwo() {
return PyModule_Create(&moduleDef);
}
查看此代码时,有几点需要注意
- 除了末尾的
PyInit_plustwo
之外,所有函数和结构都是static
的。 - 与
addtwo
模块函数对应的函数名为plustwo_addtwo
,因为模块函数与模块名称不同。 - 模块只有一个函数,但
PyMethodDef
数组有两个元素。第二个元素定义了一个null
函数,如果不存在,代码将无法工作。 PyMethodDef
的第三个参数是METH_O
,它指定函数只接受一个参数。但在代码中,plustwo_addtwo
接受两个参数:一个表示模块的PyObject
和一个表示输入参数的PyObject
。
要作为扩展模块,此代码必须编译为动态库(Windows 上为 plustwo.dll,Linux 和 macOS 上为 plustwo.so)。在 Windows 上,plustwo.dll 必须重命名为 plustwo.pyd,这标识该文件为 Python 动态模块。在 Linux 上,*.so 后缀可以保持不变。
创建扩展模块后,你可以通过打开 Python 提示符进行测试。然后你可以导入 plustwo
模块并调用 addtwo
函数,如下所示
python
>>> import plustwo
>>> x = plustwo.addtwo(5)
>>> x
7
5. 历史记录
- 2023年8月2日:首次提交
- 2023年8月4日:修复了代码标签
- 2023年8月9日:添加了对
PyFinalize
的调用