如何在C++里调用python详细教程

最近在做有关无人机视觉的项目,由于superpoint网络和ros系统的联动比较困难复杂,故最后决定直接将superpoint的代码集成到C++里,用C++直接去调用,省去了节点之间通信的繁琐,故在此记录一下我一点一点摸索的历程。

什么是PyObject?

在一切的开始,你务必要知道什么是PyObject,简单来说,一切python里的东西都是PyObject,无论你是想要传参,还是接受返回值,还是调用某个函数,某个类等等,他们都是PyObject,你也必须用一个PyObject类型的指针来接受或传递他们,下面给出PyObject的定义

// object.h
/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
};
// pytypedefs.h
typedef struct _object PyObject;

// Include/pytypedefs.h
typedef struct _typeobject PyTypeObject;
// Include/cpython/object.h
struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; 
    Py_ssize_t tp_basicsize, tp_itemsize; 
    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; 
                                    
    reprfunc tp_repr;
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;
    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    PyBufferProcs *tp_as_buffer;
    unsigned long tp_flags;
    const char *tp_doc; 
    traverseproc tp_traverse;
    inquiry tp_clear;
    richcmpfunc tp_richcompare;
    Py_ssize_t tp_weaklistoffset;
    getiterfunc tp_iter;
    iternextfunc tp_iternext;
    PyMethodDef *tp_methods;
    PyMemberDef *tp_members;
    PyGetSetDef *tp_getset;
    PyTypeObject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; 
    inquiry tp_is_gc; 
    PyObject *tp_bases;
    PyObject *tp_mro; 
    PyObject *tp_cache; 
    void *tp_subclasses;  
    PyObject *tp_weaklist; 
    destructor tp_del;
    unsigned int tp_version_tag;
    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
    unsigned char tp_watched;
};

其中你需要重点关注的是ob_type这个指针,我们后续常常用cout<<p->ob_type->tp_name来输出这个成员类型的名称,好帮助我们判断接下来应该使用什么类型对应的处理函数,比如list和tuple就是完全两种不同的数据结构,要调用两种数据类型来处理

python初始化

C++调用python的本质原理其实是启动一个python虚拟机,所以在调用python代码之前需要先对这个虚拟机进行一下初始化

首先要包含一下python的头文件,这个头文件只要你下载了python就是默认都有的,他的路径是/usr/include/pythonx.x(你的版本)/Python.h。一般情况下只需要#include<Python.h>就可以了,但是如果你有很多个python,可能会出现混淆,这个时候就建议你用一下绝对路径来指定python的头文件

一般情况下初始化的代码如下:

#include<Python.h>
int main()
{
    Py_initlize();
    import_array();//报错的话就把import_array替换成_import_array
    return 0;
}

这里说一下这个import_array的事。他是对array数组进行初始化,不管你后面用不用到array都要调用一下这个函数,但是我在写代码的时候发现直接调用import_array会报错,网上查了一下说是import_array是一个宏定义,有一个返回值,因为这个返回值导致了问题,而import_array是调用了_import_array作为核心判断逻辑的,所以可以用_import_array进行代替,也可以修改一下import_array的宏定义,让他不要有返回值,具体操作网上有很多,读者如果遇到可以自行查阅一下

接着需要添加一下文件路径,这个路径就是你想要调用的python程序的绝对路径

PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('your route/')");//你要调用的python程序的路径
PyRun_SimpleString("print('route append successfully')");

这里的PyRun_SimpleString函数可以根据字面意思理解,我觉得应该没有什么很难理解的地方,值得注意的就是,python正常是不区分单引号和双引号的,但在C++里如果你想调用python就只能在双引号里使用单引号,不能混淆

最后选择你想打开的py文件,文件名不需要带py

PyObject *pmodule=PyImport_ImportModule("demo");//打开你想要执行的python程序

python调用类及参数构建

由于本项目没有用到python函数的调用,用的都是类成员和方法的调用,故有关函数的调用不多赘述,有需要的读者可以上网上查阅,网上有很多

要调用一个类,首先需要找到这个类,并将其返回值存在一个PyObject类型的指针中,比如我下面这个例子

PyObject *superpointfrontend=PyObject_GetAttrString(pmodule,"SuperPointFrontend");//找到superpointfrontend这个类,并传回指针

pmodule就是前面指向这个程序的指针

找到这个类的话,接下来就是要对其进行初始化了,初始化在大多数情况下不可避免的要传入参数,所以这里说一下如何构建传入参数。

PyObject *argofsuperpoint=Py_BuildValue("(siddi)",weights_path,nms_dist,conf_thresh,nn_thresh,cuda);

利用Py_BuildValue函数,将返回值传回一个PyObject里,它的函数原型如下

PyObject* Py_BuildValue(const char *format, ...);

其中format是格式化字符串,用于指定后面的参数类型,对应关系如下

"i": 整数

"s": 字符串(const char*

"d": 双精度浮点数

"f": 单精度浮点数

"O": 指向 PyObject 的指针

"()": 元组

"{}": 字典

"[]": 列表

后面是可变参数,参数的数量根据你输入的格式化字符串决定,所以上面的例子中,(siddi)就表示我要传入一个第一个元素是字符串,第二个元素是int,第三、四个元素都是double,最后是一个int的元组类型。

构建好参数之后,就可以愉快的调用并初始化类成员了,利用PyEval_CallObject函数即可

PyObject *fe=PyEval_CallObject(superpointfrontend,argofsuperpoint);

第一个参数是指向这个类的指针,第二个参数是指向参数的指针。用一个PyObject来接受返回值。

接着就要调用类的方法了,利用的是PyObject_CallMethod函数,这个函数的传参有两种方式,我会在下面说明

PyObject *left=PyObject_CallMethod(fe,"run","(O)",pyimg0);

来看一下他的函数原型

PyObject* PyObject_CallMethod(PyObject *o, const char *name, const char *format, ...);

第一个参数就是指向类的指针,这个指针是你初始化完之后的指针,第二个参数是你想要运行的方法的名字,第三个参数是格式化字符串,决定了如何传参,后面的都是可变参数,同上面所说一样

第一种方式,就是利用上面构造参数的方法,将参数构造好作为一个PyObject传入,例子所展示的方法就是这种方法,“pyimg0”这个参数提前被我构造好了

第二种方法,就是直接像Py_BuildValue函数一样传参,格式化字符串直接指定要传的参数,比如

PyObject *show=PyObject_CallMethod(test,"chuancan","sid","hahaha",100,3.1415926);

这样也是OK的

调用完毕之后,就该把返回的数据想办法转换成C++的格式,简单返回一个数还好说,如果是返回一个list,或者tuple就比较麻烦了,所以下面讲解一下面对这种情况该怎么办

首先,我们cout<<p->ob_type->tp_name看一下返回的数据类型是什么,这里我输出完之后发现返回的是一个tuple,根据python代码我知道返回值一共有三个,所以这个tuple里面有三个元素。

确定是tuple了之后,我们就需要用到PyTuple_GetItem从返回值里面提取我想要的数据了,函数原型如下

PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos);

第一个参数是我调用完方法之后的返回指针,第二个参数是我想要获取的元素的下标,从0开始算

我们用一个实例来讲解一下

PyObject leftpts,rightpts;
PyArg_Parse(PyTuple_GetItem(left,0),”O”,&leftpts);//”O”表示对象是一个python——object,这个是把left这个元组的第0个元素,也就是pts返回给leftpts
PyArg_Parse(PyTuple_GetItem(right,0),”O”,&rightpts);//”O”表示对象是一个python——object

需要先初始化一个PyObject,用来接受我获取的数据,数据类型仍然是一个PyObject类型,这个根据实际情况来,因为我返回的是一个list,所以后续我还要用list来处理它。根据这个例子,相信读者也很容易理解PyArg_Parse函数的作用,就是把一个元素转化成C++支持的数据结构类型,不过我这里貌似有点多余了……毕竟当时也是摸索着写的,在写这篇文章的过程中理解也在不断加深。

接着就是将这些内容提取出来,下面直接附上一段代码,代码是AI写的,我们可以看一下

    int sizepts0=PyObject_Size(leftpts);
    int sizepts1=PyObject_Size(rightpts);
 
    PyObject *iterpts0=PyObject_GetIter(leftpts);
    PyObject *iterpts1=PyObject_GetIter(rightpts);
    //输出左目的pts
    while(true)
    {
        PyObject *next=PyIter_Next(iterpts0);//数据的行数,在pts里分别为1,2,3行
        if(!next)
        {
            break;
        }
        if(!PyList_Check(next))
        {
            int foosize0=PyObject_Size(next);//这一行的大小,也就是列数
            cout<<foosize0;
            PyObject *_iterpts0=PyObject_GetIter(next);//索引到这一行的第一个元素
            while(true)
            {
                PyObject *next2=PyIter_Next(_iterpts0);//利用迭代器逐个输出
                if(!next2)
                {
                    break;
                }
                if(!PyList_Check(next2))
                {
                    double foo=PyFloat_AsDouble(next2);
                    cout<<foo<<" ";
                }
            }
            cout<<endl;
        }
    }

整段代码的核心逻辑说白了就是剥洋葱,一层一层抽丝剥茧,其实根据这段代码,我们可以推知他的数据结构

上面的逻辑相当于是一开始可以获得head指针,根据这个指针可以拿到一共有几个nodehead,接着逐层遍历,可以吧node1.1~node3.3给遍历完,带一点数据结构的知识

里面涉及到的函数我认为都可以根据函数名称推断功能,就不多讲了

变量与虚拟资源的释放

在最终调用完python之后,需要把涉及到的变量和python虚拟机都释放掉,避免发生内存泄漏的情况,用到的函数也很简单

Py_DECREF(p);//p:待释放的变量
Py_Finalize();//释放python虚拟机

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇