【翻译】将Python嵌入到另一个应用程序中--from官网

发布 | 2018-11-01 | 编程学习 | 1136 浏览

最近因为项目上需要用C++调用pytorch训练好的模型,不得已开始查这方面的资料,正好看到官方文档的第五章将Python嵌入到另一个应用程序中有讲到这方面的知识,正好翻译一下,也贡献给有需要的小伙伴。(注意:本章中主要讲到的是将python嵌入C程序中)
ps: 文中部分翻译可能不太恰当,欢迎小伙伴们进行批评指正~
另ps:本废柴少女不仅废柴,还懒。So直接从我的CSDN账号里原封不动地copy过来了...

5.在另一款应用程序中植入python

在之前的章节中已经讨论了如何扩展python,即,如何给python附加一个C函数库来扩展其功能。实际上,反过来做也是可行的:通过植入python来丰富你的C/C++程序。通过植入python你可以在你的程序中实现一些你用python编写的功能,而不是C/C++。这可以用来满足多种需求,其中一个例子是允许用户根据自己的需要写一些python脚本来定制应用程序。当然,如果一些功能使用python编写起来要更容易,你也可以这么做。

嵌入python类似于扩展它,但并不完全如此。区别在于,当你扩展python时,应用程序中的主程序依然是依赖于python解释器;而当你需要嵌入python时,你的主程序实际上已经和python没什么关系了都——相反,应用程序中的某些部分只会偶尔需要调用python解释器来运行其中的python代码。

因此,如果你正要嵌入python,你得提供你自己的主程序。你的主程序必须要做的事情之一是你必须去初始化python解释器。起码你得调用你的Py_Initialize()函数。还有一些可选的调用将命令行参数传给python。之后,你就可以在你应用程序的任何部位去调用这个解释器了。

调用这个解释器的途径有很多:你可以传给PyRun_SimpleString()一个包含python语句的字符串,或者你也可以传给PyRun_SimpleFile()一个stdio文件指针和一个文件名(这仅用于识别错误消息)。此外,你还可以调用前几章中描述得较低级别的操作来构造和使用python对象。

你可以在发行版的源代码下的 Demo/embed/目录中找到一个嵌入python的简单示例。

另请参阅:

Python/C API 参考手册

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

5.1 the very high level 接口

嵌入python的最简单形式是使用the very high level 接口,这个接口不需要与应用程序直接交互,就可以被用来执行python脚本。例如,这可以用于对文件执行某些操作。

#include <Python.h>

int main(int argc, char *argv[])
{
  Py_SetProgramName(argv[0]);  /* optional but recommended */
  Py_Initialize();
  PyRun_SimpleString("from time import time,ctime\n"
                     "print 'Today is',ctime(time())\n");
  Py_Finalize();
  return 0;
}

Py_SetProgramName()函数应该在Py_Initialize()函数之前被调用,目的是告知解释器python运行时库的路径。接下来,python解释器会通过Py_Initialize()函数进行初始化,然后执行硬编码的Python脚本,打印日期和时间。随后,调用Py_Finalize()函数关闭解释器,然后程序结束。在实际的程序中,你可能希望从另一个源(可能是文本编辑器例程、文件或数据库)获得python脚本。但从文件中获取python代码的更好方式是使用PyRun_SimpleFile()函数,这样就省去了分配内存空间和加载文件内容的麻烦。

5.2 超越非常高水平的嵌入:概述

高级接口的确可以使你从应用程序中执行任意的python代码片段,但不得不说,交换数据值相当麻烦。如果你想要这样做,你应该使用更低级别的调用。当然,如果你不在乎编写更多更长的C代码,那你几乎可以实现任何目标。

值得注意的是,扩展python和嵌入python是几乎完全相同的任务,尽管他们的出发点是不同的。前几章讨论过的大多数主题依然有效。为了说明这一点,我们不妨以从python到C的扩展代码为例,看它究竟实际做了什么:
1)将数据值从python转换为C
2)使用转换后的值对C例程执行函数调用
3)将调用中的数据值从C转换成Python

当嵌入python时,这个接口代码做的工作如下:
1)将数据值从C转换为python
2)使用转换后的值对python接口例程执行函数调用
3)将调用中的数据值从python转换为C

如你所见,数据转换步骤简单进行了交换以适应跨语言传输时的不同转换方向。唯一的区别在于两种数据转换间调用的例程。当你需要扩展python时,调用C例程;当你需要嵌入python时,调用python例程。

本章我们不讨论如何将数据从python转换为C,反之亦然。此外,正确地使用引用和处理错误是可以理解的。这些方面与扩展解释器并没什么太大区别,你可以参考前面的章节以获得所需的信息。

5.3 纯粹的嵌入

下面的第一个程序目的是执行python脚本中的一个功能。正如我们在5.1节中提到的,python解释器不会直接与应用程序交互(但这点在下一节中将有所不同)

运行python脚本中定义的函数代码如下:

#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 = PyString_FromString(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 = PyInt_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", PyInt_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;
    }
    Py_Finalize();
    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 = PyString_FromString(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);

在对解释器进行初始化之后,使用PyImport_Import()加载脚本。这个例程需要一个python字符串作为它的参数,该参数是通过使用 PyString_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 要么为空,要么包含对函数返回值得应用。在检查完值之后,一定要记得释放引用。

5.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 Py_BuildValue("i", numargs);
}

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

在main()函数之上插入上述代码。另外,在Py_Initialize() 之后直接插入以下两条语句:

numargs = argc;
Py_InitModule("emb", EmbMethods);

这两行代码初始化了numargs变量,并使嵌入式python解释器能够访问emb.numargs()函数。有了这些扩展,python脚本可以做以下事情:

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

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

5.5 在C++中嵌入python

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

5.6 在类Unix系统下的编译和链接

为了将python解释器嵌入到应用程序中,找到传递给编译器(和链接器)的正确标志并不是件小事,这是因为python需要加载作为C动态扩展来实现(.so文件)的库模块,并将其链接到应用程序中。

为了找到所需的编译器和链接器标志,可以执行在安装过程中生成的pythonX.Y-config 脚本(也可以使用python-config脚本)。这个脚本有好几个选项,下面的选项可能对你有用:
当你编译时pythonX.Y-config --cflags将给你推荐的标志:

$ /opt/bin/python2.7-config --cflags
-I/opt/include/python2.7 -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes

当你链接时,pythonX.Y-config --ldflags将给你推荐的标志:

$ /opt/bin/python2.7-config --ldflags
-L/opt/lib/python2.7/config -lpthread -ldl -lutil -lm -lpython2.7 -Xlinker -export-dynamic

注意:为了避免在几个python安装之间(特别是在系统python和你自己编译的python)产生混淆,建议使用pythonX.Y-config 的绝对路径,如上例所示。

如果此步骤对你来说并不适用(它并不能保证适用所有的类unix平台;但我们同样欢迎bug报告),你必须阅读系统关于动态链接和/或检查python的Makefiles(使用sysconfig.get_makefile_filename()查找其位置)和编译选项。在这种情况下,sysconfig模块是一个有用的工具,可以通过编程方式提取你想要组合在一起的配置值。例如:

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

--------------------------------------------------------------------------------------------------------这是一条严肃的分割线

翻译完也没能搞明白C++是怎么调用Python的,确实是条废柴没错了,哭!

© 著作权归作者所有

本文由 豆末 创作,采用 知识共享署名4.0 国际许可协议进行许可,本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。

吐槽列表

cialis for sale in usa, 2019-03-25 回复

Good way of describing, and pleasant paragraph to take information on the topic
of my presentation subject, which i am going to deliver in college.

cialis, 2019-04-30 回复

Incredible points. Sound arguments. Keep up the amazing work.

吐槽一下吧

取消回复

*选项为必填