65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2023年8月2日

CPOL

15分钟阅读

viewsIcon

18395

downloadIcon

473

本文解释了 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 数据类型的实例表示。模块由 PyObjects 表示,函数和方法由 PyObjects 表示,变量也由 PyObjects 表示。

本文将不讨论 Python API 中的所有函数,甚至大多数函数。相反,我们将这些函数分为两类

  1. 基本函数 - 访问模块、方法和属性的函数
  2. 对象创建和转换 - 创建 PyObjects 并将其转换为其他类型的函数

在探讨这些函数之后,本节将介绍读取简单 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。应用程序可以通过调用 PyINCREFPyXINCREF 来增加引用计数。两者都接受指向 PyObject 的指针,第一个函数只有在指针不为 NULL 时才应调用。第二个函数 PyXINCREF 可以在指针为 null 时调用。

当不再需要 PyObject 时,应用程序应调用 PyDECREFPyXDECREF 来减少对象的引用计数。一旦计数达到 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_FromStringstring 创建 Unicode PyObject。你还可以通过调用 PyBytes_FromStringstring 创建字节 PyObject

显示属性的 string 也比较复杂。PyObject_Str 返回包含对象 stringPyObjectPyUnicode_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.pyplus 函数的样子。

def plus(a, b):
    return a + b

要从 embedding.cpp 构建应用程序,你需要告诉编译器 Python 安装目录的 include 文件夹中的头文件和 libs 文件夹中的库文件。当我在我的系统上运行应用程序时,输出如下

Result: 11

4. 创建 Python 扩展

Python 解释器可以访问包括 osdatetimestring 在内的内置模块。使用 Python API,我们可以添加名为扩展模块的新内置模块。与常规模块不同,扩展模块是用 C 或 C++ 编写的动态库。

本讨论将逐步讲解一个名为 plustwo 的扩展模块的开发。它包含一个名为 addtwo 的函数,该函数接受一个数字并返回该数字加 2 的和。

python
>>> import plustwo
>>> x = plustwo.addtwo(5)
>>> x
7

编程扩展模块很难,因为函数需要有特殊的名称,并且必须提供特殊的数据结构。要理解这个过程,你需要注意三点

  1. 如果所需的模块是 modname,代码必须定义一个名为 PyInit_modname 且不接受任何参数的函数。对于示例,模块名为 plustwo,因此代码定义了一个名为 PyInit_plustwo 的函数。
  2. 为了描述模块,代码必须创建一个 PyModuleDef 结构并设置其字段。这些字段标识模块的名称、其文档和其方法。
  3. 对于模块中的每个函数,代码必须创建一个 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 函数,该函数提供在调用模块函数时要执行的代码。编写此函数时,需要记住四个规则

  1. 如果模块函数与模块名称不同,则 C 函数的名称应设置为 modname_funcname,其中 modname 是模块的名称,funcname 是函数的名称。
  2. 如果模块函数与它所在的模块名称相同,则 C 函数的名称应设置为 modname
  3. 函数接受的参数数量由 PyMethodDefml_flags 参数决定。
  4. 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 的调用
© . All rights reserved.