跳至主要內容

Qt C++ Python 混合编程测试文档

CK...大约 12 分钟技巧总结QtC++Python混合编程

Qt C++ Python 混合编程测试文档

环境版本

  • Qt:5.9.0 (MSVC 2017 64bit)
  • Python: 3.10.2 (64 bit)

开发步骤

将 Python 集成到 Qt 中

安装Python环境(略)

Qt 配置 Python 解释器

选项——环境——外部工具

添加工具:Python3

构建执行档:python.exe路径(我这里是 D:\python\python.exe)

参数:%{CurrentDocument:FilePath}

工作目录:%{CurrentDocument:Path}

image-20220311095425155

添加 Python 脚本文件

打开项目文件,选择添加 Python File 文件。

image-20220311100000989

本文将用下面的两个函数作为测试函数,分别接受一个 Python 列表和一个 numpy.darray, 将他们排序后并返回。具体如下所示:

注意:这里不能命名文件为 test.py。 会和python定义的test.py重复,运行会报错找不到.py文件。所以不要将.py文件命名为test.py。

# list_test.py
import numpy


def sort_by_list(mylist):
    print('使用python列表')
    mylist.sort()
    return mylist

def sort_by_numpy_list(myarray):
    # if type(myarray) is not numpy.ndarray:
    #     return
    print('使用numpy数组')
    myarray.sort()
    return myarray

添加 Python 程序文件后,可用测试程序,按照下面的方式运行,检测是否添加文件成功。

image-20220311161849917

Qt 调用Python脚本

pro 导入Python环境

INCLUDEPATH += -I D:\python\include
LIBS += -LD:\python\libs -lpython310

注意:

  • += -LD:\python 中的 LD 不可分开
  • -lpython310 根据自己python版本决定,比如python 3.9 为 -lpython39
  • 我的python路径如下所示:
image-20220311170559022
image-20220311170632049

C++ 调用 Python 函数接口 API介绍

本文以上文两个函数为例,分别测试参数和返回值为 list 类型和 numpy.darray 类型的函数调用。

从操作步骤上看,C++调用 Python 低层接口可以分为几个阶段

  • 初始化Python解释器
  • 从C++到Python转换数据
  • 用转换后的数据做参数调用Python函数
  • 把函数返回值转换为C++数据结构

初始化Python解释器

#include <Python.h>
...
Py_Initialize();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.argv = ['python.py']");
PyRun_SimpleString("sys.path.append('./')");

初始化Python后,可以通过 int PyRun_SimpleString(const char *command) 函数令解释器执行任意 python 代码。这种叫做高层接口。高层接口虽然方便,但很难与C/C++交换数据。所以对于复杂需求,应该使用低层接口。虽然需要多写很多C代码,但可以灵活的实现很多复杂功能。

C++ 数据转化为 PyObject

PyObject 为 C++ 中 Python 的数据类型,传入 Python 函数的参数和从 Python 函数得到的返回值,必须以 PyObject 的形式存在。

基本数据类型转换

  • 可以采用下面的库函数进行基本数据的转换:
PyObject* PyLong_FromLong(long v)
PyObject* PyBool_FromLong(long v)
PyObject* PyFloat_FromDouble(double v)
  • 可以采用 Py_BuildValue() 函数进行转换

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

img
img

下面给出几个例子

Py_BuildValue("")  None
 
Py_BuildValue("i", 123)  123
 
Py_BuildValue("iii", 123, 456, 789)  (123, 456, 789)
 
Py_BuildValue("s", "hello")  'hello'
 
Py_BuildValue("ss", "hello", "world")  ('hello', 'world')
 
Py_BuildValue("s#", "hello", 4) 'hell'
 
Py_BuildValue("()")  ()
 
Py_BuildValue("(i)", 123)  (123,)
 
Py_BuildValue("(ii)", 123, 456) (123, 456)
 
Py_BuildValue("(i,i)", 123, 456) (123, 456)
 
Py_BuildValue("[i,i]", 123, 456) [123, 456] 
 
Py_BuildValue("{s:i,s:i}", "abc", 123, "def", 456) {'abc': 123, 'def': 456}
 
Py_BuildValue("((ii)(ii)) (ii)", 1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
  • 列表、元组类型转换 PyList、PyTuple

List API 简单介绍

// 判断是否是一个Python List(列表)
int PyList_Check(PyObject *p) 
// 创建一个列表
PyObject* PyList_New(Py_ssize_t len) 
// 获取列表元素的个数 len(list)
Py_ssize_t PyList_Size(PyObject *list) 
// 和PyList_Size 一样,但是就是没有错误检查
Py_ssize_t PyList_GET_SIZE(PyObject *list) 
// 从列表里面获取一个元素,计数器不会加1
PyObject PyList_GetItem(PyObject list, Py_ssize_t index) 
// 和PyList_GetItem一样,但是就是没有错误检查
PyObject PyList_GET_ITEM(PyObject list, Py_ssize_t i) 
// 设置别表指定位置的值,下标的所在的位置必须是有值的,并且是有效的
int PyList_SetItem(PyObject list, Py_ssize_t index, PyObject item) 
// 和PyList_SetItem一样,但是就是没有错误检查
void PyList_SET_ITEM(PyObject list, Py_ssize_t i, PyObject o) 
// 在列表指定位置插入值 list.insert(index, item)
int PyList_Insert(PyObject list, Py_ssize_t index, PyObject item) 
// 在列表尾部追加值 list.append(item)
int PyList_Append(PyObject list, PyObject item) 
// 获取列表里面一段切片数据,一段指定范围的数据 list[low:higt]
PyObject PyList_GetSlice(PyObject list, Py_ssize_t low, Py_ssize_t high) 
// 设置列表分片数据,指定列表范围的数据 list[low:higt] = itemlist
int PyList_SetSlice(PyObject list, Py_ssize_t low, Py_ssize_t high, PyObject itemlist) 
// 对列表数据进行排序 list.sort()
int PyList_Sort(PyObject *list) 
// 把列表里面的所有数据反转 list.reverse()
int PyList_Reverse(PyObject *list) 
// 将Python列表转为Python元组 tuple(list)
PyObject PyList_AsTuple(PyObject list) 

具体使用如下所示:

// 将C++ 数组 array_1 转化为 PyList 

PyObject *PyList = PyList_New(SizeOfList) // 定义一个长度为 SizeOfList 的列表
for(int Index_i = 0; Index_i < SizeOfList; Index_i++)
{
    PyList_SetItem(PyList, Index_i, Py_BuildValue("i", arrayA[Index_i])); 
}

Tuple API 简单介绍

// 判断是否是一个元组对象
int PyTuple_Check(PyObject *p) 
// 创建Python元组对象,注意元组创建是必须设置长度的,如果设置长度为0,则这个元组对象是一个空的元组
PyObject* PyTuple_New(Py_ssize_t len) 
// 获取元组的长度,即元组的大小
Py_ssize_t PyTuple_Size(PyObject *p) 
// 和PyTuple_Size一样,只不过这个方法没有错误检查的机制
Py_ssize_t PyTuple_GET_SIZE(PyObject *p) 
// 获取元组内指定下标的值
PyObject PyTuple_GetItem(PyObject p, Py_ssize_t pos) 
// 和PyTuple_GetItem一样,只不过这个方法没有错误检查的机制
PyObject PyTuple_GET_ITEM(PyObject p, Py_ssize_t pos) 
// 获取分片数据 p[lwo, higt]
PyObject PyTuple_GetSlice(PyObject p, Py_ssize_t low, Py_ssize_t high) 
// 设置元组指定下标的值
int PyTuple_SetItem(PyObject p, Py_ssize_t pos, PyObject o) 
// 和PyTuple_SetItem一样,只不过这个方法没有错误检查的机制
void PyTuple_SET_ITEM(PyObject p, Py_ssize_t pos, PyObject o) 
// 改变元组的大小
int _PyTuple_Resize(PyObject **p, Py_ssize_t newsize) 

用法与 PyList 基本一致。

numpy.darray API 简单介绍

在使用此类型之前,必须进行一些配置:

  1. 在Pro文件中,加入下面语句,使得配置环境包括numpy/arrayobject.h:
INCLUDEPATH += -I D:\python\Lib\site-packages\numpy\core\include
  1. 添加头文件 #include <numpy/arrayobject.h>
  2. 添加函数 init() 并在 Py_Initialize();后调用函数,注意,这里可能出错,需要调整使用 Release 编译而不是 Debug 编译。
#include <numpy/arrayobject.h>
...
int init_numpy() {
    import_array();
}

...
Py_Initialize();
init_numpy();

做完初始化后,我们就可以使用 PyArrayObject 对象。先对PyArrayObject 对象做一个简单的介绍。PyArrayObject 实际上是一个结构体,结构体内包含四个元素,用来访问 Numpy Array 中的数据:

  1. int nd:Numpy Array数组的维度。
  2. int *dimensions :Numpy Array 数组每一维度数据的个数。
  3. int *strides:Numpy Array 数组每一维度的步长。
  4. char *data: Numpy Array 中指向数据的头指针。

所以当我们要访问 PyArrayObject 对象中的数据时,有:

 //对于一维 Numpy Array 数组,我们访问[i]位置处的元素的值
 PyArrayObject *array
 array->data + i*array->strides[0] 
//对于二维 Numpy Array 数组,我们访问[i, j]位置处的元素的值
 PyArrayObject *array
 array->data + i*array->strides[0] + j*array->strides[1]

我们以创建一维数组为例:

npy_intp Dims[1] = {colCnt}; //给定维度信息

for(int i = 0;i < colCnt; i++)
{
   arrayA[i]=ui->tableA->item(0,i)->text().toInt();
}
PyObject *PyArray  = PyArray_SimpleNewFromData(1, Dims, NPY_INT, arrayA);

调用 Python 函数

我们首先需要指定 模块和函数:

PyObject* PyModule = PyImport_ImportModule("list_test"); // 指定模块 list_test.py
PyObject* PyFun= PyObject_GetAttrString(PyModule,"sort_by_numpy_list");// 指定函数

所有的参数通过一个元组数据结构进行传递,具体如下所示:

//定义一个Tuple对象,Tuple对象的长度与Python函数参数个数一致
PyObject *PyArg = PyTuple_New(1);
PyTuple_SetItem(PyArg, 0, PyArray);  //参数设置,PyArray 是要传递进去的参数

采用下面的方式调用函数并且获得返回值:

PyObject *PyResult  =  (PyArrayObject *)PyObject_CallObject(PyFun, PyArg);

返回值转化为 C++ 数据结构

基本数据结构

  • 可以使用基本库函数
// 使用一系列库函数转换基本变量
long PyLong_AsLong(PyObject *obj)
long PyInt_AsLong(PyObject *obj)
double PyFloat_AsDouble(PyObject *obj)
string PyString_AsString(PyObject *obj)
  • 可以使用 PyArg_Parse() 函数
int PyArg_Parse(PyObject *args, const char *format, ...)
img
img

具体使用如下所示:

int num;
int SizeOfList = PyList_Size(PyResult);//List对象的大小,这里SizeOfList = 3
for(int Index_i = 0; Index_i < SizeOfList; Index_i++)
{
    PyObject *Item = PyList_GetItem(PyResult, Index_i);//获取List对象中的每一个元
    PyArg_Parse(Item, "i", &num);
    cout << num << endl;
}
  • PyList、PyTurpe

只能通过上例,单独访问转换获得每一个元素,进而组成 C++ 数组形式。

  • numpy.darray

只能通过下例,单独访问转换获得每一个元素,进而组成 C++ 数组形式。

int num; // 临时数据存储
int SizeOfList =  PyResult->dimensions[0];//List对象的大小,这里SizeOfList = 3

for(int Index_i = 0; Index_i < SizeOfList; Index_i++)
{
    //访问数据,Index_m 和 Index_n 分别是数组元素的坐标,乘上相应维度的步长,即可以访问数组元素
    num = *(int *)(PyResult->data + Index_i * PyResult->strides[0]);
    cout << num << endl;
}

在接受 numpy.darray 返回值时,需要通过下面的方式:

 PyArrayObject *PyResult  =  (PyArrayObject *)PyObject_CallObject(PyFun, PyArg);

常见问题总结

**坑1、**首先是.py程序的名称,别命名为test.py,会和python定义的重复的,随便都想,就是别用这个名字,否则不管你里面写什么函数一律找不到,以为它压根读到的就不是这个文件。

**坑3、**怎么导入静态库: Pro文件下右键--添加库--然后外部库--然后看图--

img
img

**坑4、**如果提示你找不到 python37_d.lib 怎么办

那就把libs文件夹下的python37.lib文件,重新拷一份回来并且重命名为python37_d.lib就行了

**坑5、**提示打不开python.h文件

首先你导入库正确了,就像第三条说的那样做就行,接下来是这里的问题: 你只需要更改下构建的路径就行了,或者更简单的直接将对号去掉构建在当前工程同一个文件夹下。

img
img

**坑6、**当你做完第五步,你会发现尼玛还有问题,提示的error:error: expected unqualified-id before ';' token

将error展开说是在python中的object.h文件中的slots冲突,天哪,发生了什么?解决吧

原因:由于QT中定义了slots作为关键了,而python3中有使用slot作为变量,所以有冲突

img
img

坑7、 你还要将你的.py文件放在和QT的EXE文件在同一目录下,否则还会持续报错

坑8、 QT Creator 使用 design 修改 ui界面编译后界面未更新问题的解决。项目设置文件.pro内增加 UI_DIR=./UI,同时删除掉源代码目录中ui_*.h,clear all,->qmake->rebuilt all

坑9、 fatal error: numpy/arrayobject.h: No such file or directory:在Pro文件添加路径,详情请看上文有提到。

坑10、 Qt Creator mainwindow.obj-1: error: LNK2019

  1. 请先把已经存在的debug和release文件夹删除,重新编译,看会不会再出现这个问题;
  2. 如果还在出现,那绝对说明你的一些成员函数只有声明没有实现,或者两者不一致

官方文档与参考博文

  • 官方文档

Python/C API 参考手册open in new window

NumPy C-APIopen in new window

  • 参考博文

C++(Qt)与Python混合编程(一)open in new window

Python + C/C++ 嵌入式编程(1):多维数组Numpy.Array()在Python和C/C++文件间的传递问题open in new window

C/C++调用Python函数(CodeBlocks平台实现)open in new window

C++调用Python的API总结open in new window

Py_BuildValue()函数open in new window

Python C API的使用详解(一)open in new window

Python C API的使用详解(二)open in new window

QT调用python脚本时遇到的坑(十一大坑全有)open in new window

fatal error: numpy/arrayobject.h: No such file or directoryopen in new window