1. 在其他应用程序中嵌入 Python

之前的章节讨论了如何扩展 Python,即通过将 C 函数库附加到 Python 来扩展其功能。也可以反过来:通过在 C/C++ 应用程序中嵌入 Python 来丰富您的应用程序。嵌入功能使您的应用程序能够以 Python 而非 C 或 C++ 实现部分功能。这可以用于多种目的;一个示例是允许用户通过编写一些 Python 脚本来根据自己的需求调整应用程序。如果某些功能可以更轻松地用 Python 编写,您也可以自己使用它。

嵌入 Python 类似于扩展 Python,但又并非完全相同。区别在于,当您扩展 Python 时,应用程序的主程序仍然是 Python 解释器,而如果您嵌入 Python,主程序可能与 Python 完全无关 — 相反,应用程序的某些部分会偶尔调用 Python 解释器来运行一些 Python 代码。

因此,如果您要嵌入 Python,您将提供自己的主程序。主程序必须做的一件事是初始化 Python 解释器。至少,您必须调用 Py_Initialize() 函数。还有可选的调用来向 Python 传递命令行参数。然后,您可以在应用程序的任何部分调用解释器。

有几种不同的方式可以调用解释器:您可以将包含 Python 语句的字符串传递给 PyRun_SimpleString(),或者您可以将 stdio 文件指针和文件名(仅用于错误消息中的识别)传递给 PyRun_SimpleFile()。您还可以调用前几章中描述的较低级操作来构造和使用 Python 对象。

参见

Python/C API 参考手册

本手册提供了 Python C 接口的详细信息。这里可以找到大量必要的信息。

1.1. 非常高级的嵌入

嵌入 Python 最简单的形式是使用非常高级的接口。此接口旨在执行 Python 脚本,而无需直接与应用程序交互。例如,这可以用于对文件执行某些操作。

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* optional but recommended */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

备注

使用 #define PY_SSIZE_T_CLEAN 来指示在某些 API 中应使用 Py_ssize_t 而不是 int。自 Python 3.13 以来不再需要,但我们为了向后兼容性在此处保留它。有关此宏的描述,请参见 字符串和缓冲区

应在调用 Py_InitializeFromConfig() 之前调用设置 PyConfig.program_name,以告知解释器 Python 运行时库的路径。接下来,使用 Py_Initialize() 初始化 Python 解释器,然后执行一个硬编码的 Python 脚本,该脚本打印日期和时间。之后,调用 Py_FinalizeEx() 关闭解释器,然后程序结束。在实际程序中,您可能希望从其他来源获取 Python 脚本,例如文本编辑器例程、文件或数据库。从文件中获取 Python 代码最好通过使用 PyRun_SimpleFile() 函数来完成,这可以省去您分配内存空间和加载文件内容的麻烦。

1.2. 超越非常高级的嵌入:概述

高级接口使您能够从应用程序执行任意 Python 代码片段,但数据交换至少可以说相当麻烦。如果需要,您应该使用较低级的调用。以编写更多 C 代码为代价,您可以实现几乎任何事情。

应该指出,尽管意图不同,扩展 Python 和嵌入 Python 是非常相同的活动。前几章讨论的大部分主题仍然有效。为了说明这一点,请考虑从 Python 到 C 的扩展代码实际做了什么

  1. 将数据值从 Python 转换为 C,

  2. 使用转换后的值对 C 例程执行函数调用,以及

  3. 将调用返回的数据值从 C 转换为 Python。

当嵌入 Python 时,接口代码执行

  1. 将数据值从 C 转换为 Python,

  2. 使用转换后的值对 Python 接口例程执行函数调用,以及

  3. 将调用返回的数据值从 Python 转换为 C。

如您所见,数据转换步骤只是简单地互换,以适应跨语言传输的不同方向。唯一的区别是您在两次数据转换之间调用的例程。扩展时,您调用 C 例程;嵌入时,您调用 Python 例程。

本章将不讨论如何将数据从 Python 转换为 C 反之亦然。此外,假定对引用和错误处理的正确使用已理解。由于这些方面与扩展解释器没有区别,您可以参考前面的章节获取所需信息。

1.3. 纯嵌入

第一个程序旨在执行 Python 脚本中的一个函数。与关于非常高级接口的部分一样,Python 解释器不直接与应用程序交互(但这将在下一节中改变)。

运行 Python 脚本中定义的函数的代码是

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();
    pName = PyUnicode_DecodeFSDefault(argv[1]);
    /* Error checking of pName left out */

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyLong_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyLong_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    if (Py_FinalizeEx() < 0) {
        return 120;
    }
    return 0;
}

此代码使用 argv[1] 加载 Python 脚本,并调用 argv[2] 中命名的函数。其整数参数是 argv 数组的其他值。如果您 编译并链接 此程序(我们称完成的可执行文件为 call),并使用它执行 Python 脚本,例如

def multiply(a,b):
    print("Will compute", a, "times", b)
    c = 0
    for i in range(0, a):
        c = c + b
    return c

那么结果应该是

$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6

尽管该程序的功能而言相当大,但大部分代码用于 Python 和 C 之间的数据转换以及错误报告。关于嵌入 Python 的有趣部分始于

Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);

初始化解释器后,使用 PyImport_Import() 加载脚本。此例程需要一个 Python 字符串作为其参数,该字符串是使用 PyUnicode_DecodeFSDefault() 数据转换例程构造的。

pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */

if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

加载脚本后,使用 PyObject_GetAttrString() 检索我们正在寻找的名称。如果名称存在,并且返回的对象可调用,您可以安全地假设它是一个函数。然后程序通过像往常一样构造一个参数元组来继续。然后用以下方式调用 Python 函数

pValue = PyObject_CallObject(pFunc, pArgs);

函数返回时,pValue 要么是 NULL,要么包含对函数返回值的引用。检查值后务必释放引用。

1.4. 扩展嵌入式 Python

到目前为止,嵌入式 Python 解释器无法访问应用程序本身的功能。Python API 通过扩展嵌入式解释器来实现这一点。也就是说,嵌入式解释器通过应用程序提供的例程进行扩展。虽然听起来很复杂,但并没有那么糟糕。暂时忘记应用程序启动 Python 解释器。相反,将应用程序视为一组子例程,并编写一些粘合代码,让 Python 访问这些例程,就像您编写普通的 Python 扩展一样。例如

static int numargs=0;

/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return PyLong_FromLong(numargs);
}

static PyMethodDef emb_module_methods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef emb_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "emb",
    .m_size = 0,
    .m_methods = emb_module_methods,
};

static PyObject*
PyInit_emb(void)
{
    return PyModuleDef_Init(&emb_module);
}

将上述代码插入到 main() 函数的正上方。此外,在调用 Py_Initialize() 之前插入以下两条语句

numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);

这两行初始化 numargs 变量,并使 emb.numargs() 函数可供嵌入式 Python 解释器访问。通过这些扩展,Python 脚本可以执行以下操作

import emb
print("Number of arguments", emb.numargs())

在实际应用程序中,这些方法将向 Python 公开应用程序的 API。

1.5. 在 C++ 中嵌入 Python

在 C++ 程序中嵌入 Python 也是可能的;具体如何实现将取决于所使用的 C++ 系统的细节;通常您需要用 C++ 编写主程序,并使用 C++ 编译器编译和链接您的程序。无需使用 C++ 重新编译 Python 本身。

1.6. 在类 Unix 系统下编译和链接

找到正确的标志传递给编译器(和链接器)以将 Python 解释器嵌入到您的应用程序中并非易事,特别是因为 Python 需要加载实现为 C 动态扩展(.so 文件)并与其链接的库模块。

要找出所需的编译器和链接器标志,您可以执行作为安装过程一部分生成的 pythonX.Y-config 脚本(也可能提供 python3-config 脚本)。此脚本有几个选项,其中以下选项对您直接有用

  • pythonX.Y-config --cflags 将为您提供编译时推荐的标志

    $ /opt/bin/python3.11-config --cflags
    -I/opt/include/python3.11 -I/opt/include/python3.11 -Wsign-compare  -DNDEBUG -g -fwrapv -O3 -Wall
    
  • pythonX.Y-config --ldflags --embed 将为您提供链接时推荐的标志

    $ /opt/bin/python3.11-config --ldflags --embed
    -L/opt/lib/python3.11/config-3.11-x86_64-linux-gnu -L/opt/lib -lpython3.11 -lpthread -ldl  -lutil -lm
    

备注

为避免不同 Python 安装之间(尤其是系统 Python 和您自己编译的 Python 之间)的混淆,建议您使用 pythonX.Y-config 的绝对路径,如上例所示。

如果此过程对您不起作用(不能保证适用于所有类 Unix 平台;但是,我们欢迎 错误报告),您将必须阅读您系统关于动态链接的文档和/或检查 Python 的 Makefile(使用 sysconfig.get_makefile_filename() 查找其位置)和编译选项。在这种情况下,sysconfig 模块是一个有用的工具,可以以编程方式提取您想要组合在一起的配置值。例如

>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl  -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'