1. 将 Python 嵌入到其他应用程序中¶
前面的章节讨论了如何扩展 Python,也就是如何通过向 Python 添加 C 函数库来扩展 Python 的功能。反过来也可以:通过将 Python 嵌入到你的 C/C++ 应用程序中来丰富你的应用程序。嵌入使你的应用程序能够使用 Python 而不是 C 或 C++ 来实现应用程序的某些功能。这可以用于许多目的;一个例子是允许用户通过编写一些 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 起,这不是必须的,但为了向后兼容,我们在此保留它。有关此宏的描述,请参阅 字符串和缓冲区。
设置 PyConfig.program_name
应该在 Py_InitializeFromConfig()
之前调用,以告知解释器 Python 运行时库的路径。接下来,使用 Py_Initialize()
初始化 Python 解释器,然后执行一个硬编码的 Python 脚本,该脚本打印日期和时间。之后,Py_FinalizeEx()
调用关闭解释器,然后程序结束。在实际程序中,你可能希望从其他来源(例如文本编辑器例程、文件或数据库)获取 Python 脚本。最好使用 PyRun_SimpleFile()
函数从文件中获取 Python 代码,这可以省去你分配内存空间和加载文件内容的麻烦。
1.2. 超越非常高层次的嵌入:概述¶
高层次的接口使你能够从应用程序中执行任意的 Python 代码片段,但是交换数据值至少可以说非常麻烦。如果你想要那样做,你应该使用较低级别的调用。以必须编写更多 C 代码为代价,你可以实现几乎任何事情。
应该注意的是,尽管意图不同,但扩展 Python 和嵌入 Python 是非常相同的活动。前面章节中讨论的大多数主题仍然有效。为了说明这一点,请考虑从 Python 到 C 的扩展代码实际做了什么
将数据值从 Python 转换为 C,
使用转换后的值对 C 例程执行函数调用,以及
将来自调用的数据值从 C 转换为 Python。
当嵌入 Python 时,接口代码会执行
将数据值从 C 转换为 Python,
使用转换后的值对 Python 接口例程执行函数调用,以及
将来自调用的数据值从 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_FromString()
数据转换例程构造的。
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 EmbMethods[] = {
{"numargs", emb_numargs, METH_VARARGS,
"Return the number of arguments received by the process."},
{NULL, NULL, 0, NULL}
};
static PyModuleDef EmbModule = {
PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
NULL, NULL, NULL, NULL
};
static PyObject*
PyInit_emb(void)
{
return PyModule_Create(&EmbModule);
}
将上面的代码插入到 main()
函数的正上方。另外,在调用 Py_Initialize()
之前插入以下两条语句
numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);
这两行初始化了 numargs
变量,并使嵌入式 Python 解释器可以访问 emb.numargs()
函数。通过这些扩展,Python 脚本可以执行如下操作:
import emb
print("Number of arguments", emb.numargs())
在实际的应用程序中,这些方法会将应用程序的 API 公开给 Python。
1.5. 在 C++ 中嵌入 Python¶
也可以将 Python 嵌入到 C++ 程序中;具体如何操作取决于所使用的 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'