1. 程式人生 > >Qt+Python混合程式設計

Qt+Python混合程式設計

專案使用Qt搭建了一個數據庫軟體,這個軟體還需要有一些資料分析、特徵重要度計算、效能預測等功能,而python的機器學習第三方庫比較成熟,使用起來也比較便捷,因此這裡需要用到Qt(c++)+python混合程式設計,在此記錄一下相關方法與問題,以方便自己與他人。

本專案使用的是QtCreator(Qt5.5.0)+VisualStudio2013+python3.6.5搭建。其他版本只要版本是正確對應的,都大同小異。

準備工作

假設你已經正確安裝了Qt和python,由於Qt中的slots關鍵字與python重複,這裡我們需要修改一下檔案../Anaconda/include/object.h(注意先將原檔案備份): 原檔案(448行):

typedef struct{
    const char* name;
    int basicsize;
    int itemsize;
    unsigned int flags;
    PyType_Slot *slots; /* terminated by slot==0. */
} PyType_Spec;

修改為:

typedef struct{
    const char* name;
    int basicsize;
    int itemsize;
    unsigned int flags;
    #undef slots // 這裡取消slots巨集定義
PyType_Slot *slots;/* terminated by slot==0. */ #define slots Q_SLOTS // 這裡恢復slots巨集定義與QT中QObjectDefs.h中一致 } PyType_Spec;

完成上述工作後我們需要在.pro檔案中加入python的路徑資訊(我的python路徑是Y:/Anaconda):

INCLUDEPATH += -I  Y:/Anaconda/include

LIBS += -LY:/Anaconda/libs -lpython36

python3.dllpython36.dllpythoncom36.dll

pywintypes36.dll放到.exe目錄下。

Qt呼叫python指令碼

python檔案

建立一個python指令碼放在release專案目錄下,這裡我們新建了一個kde.py,其中包含無返回值函式plotKDE(x, column, kernel, algorithm, breadth_first, bw, leaf_size, atol, rtol, title)用於繪製核KDE曲線與直方圖和有返回值函式loadData()用於讀取本地.csv檔案,影象繪製效果如下所示: Loading...

kde.py部分程式碼如下(為方便表達,後續步驟中我們將plotKDE函式簡寫為plotKDE(x, column, kernel)):

import csv
import os
import matplotlib as mpl
from matplotlib import pyplot as plt
import numpy as np
from sklearn.neighbors import KernelDensity

mpl.use('TkAgg')
mpl.rcParams['font.sans-serif'] = ['SimHei']
mpl.rcParams['axes.unicode_minus'] = False

BASE_DIR = os.path.dirname(__file__)
file_path = os.path.join(BASE_DIR, 'train.csv')

def plotKDE(x, column, kernel='gaussian', algorithm='auto', breadth_first=1,
            bw=30, leaf_size=40, atol=0, rtol=1E-8, title):
    # kde
    x_plot = np.linspace(min(x), max(x), 1000).reshape(-1, 1)
    x = np.mat(x).reshape(-1, 1)
    fig, ax = plt.subplots()
    kde = KernelDensity(bandwidth=bw, algorithm=algorithm, kernel=kernel,
                        atol=atol, rtol=rtol, breadth_first=breadth_first,
                        leaf_size=leaf_size).fit(x)
    log_dens = kde.score_samples(x_plot)
    ax.hist(x, density=True, color='lightblue')
    ax.plot(x_plot[:, 0], np.exp(log_dens))

    plt.title(title[column])
    plt.show()

def loadData():
    x = []
    with open(file_path, 'rt') as csvfile:
        reader = csv.reader(csvfile)
        for line in reader:
            tmp = list(map(float, line[4:]))
            x.append(tmp)
    return x

Qvector轉pyObject型別

此處新建了一個類用於將Qt中儲存的資料的QVector<double>型別等轉換為用於python指令碼的QObject型別。QVector<QVector<double>>的轉換方法可以此類推。

PyObject *Utility::UtilityFunction::convertLabelData(QVector<double> *labels)
{
    int labelSize = labels->size();
    PyObject *pArgs = PyList_New(labelSize);

    for (int i = 0; i < labelSize; ++i) {
        PyList_SetItem(pArgs, i, Py_BuildValue("d", (*labels)[i]));
    }
    return pArgs;
}

Qt呼叫python指令碼

在Qt專案中呼叫kde.pyplotKDE函式顯示影象(有輸入,無返回值):

// 初始化
Py_Initialize();
if (!Py_IsInitialized()) {
    printf("inititalize failed");
    qDebug() << "inititalize failed";
    return ;
}
else {
    qDebug() << "inititalize success";
}

// 載入模組,即loadtraindata.py
PyObject *pModule = PyImport_ImportModule("kde");
if (!pModule) {
    PyErr_Print();
    qDebug() << "not loaded!";
    return ;
}
else {
    qDebug() << "load module success";
}

// QVector<double> pKDE中存放了選中列的所有資料
PyObject *pKDEdata = Utility::UtilityFunction::convertLabelData(&pKDE); // 型別轉換
PyObject *pArg = PyTuple_New(3);
PyTuple_SetItem(pArg, 0, pKDEdata);
// int column表示選中的列的索引
PyTuple_SetItem(pArg, 1, Py_BuildValue("i", column));
// Qstring kernel表示核型別
PyTuple_SetItem(pArg, 2, Py_BuildValue("s", kernel.toStdString().c_str()));

// 載入函式loadData()
PyObject *pFunc = PyObject_GetAttrString(pModule, "plotKDE");

if (!pFunc) {
    printf("get func failed!");
}
else {
    qDebug() << "get func success";
}

PyObject_CallObject(pFunc, pArg);

Py_DECREF(pModule);
Py_DECREF(pFunc);
Py_Finalize();

在Qt專案中呼叫kde.pyloadData函式讀取本地資料(無輸入,有返回值):

Py_Initialize();
QVector<QVector<double> > *trainData; // 儲存python指令碼讀入的資料

if (!Py_IsInitialized()) {
    printf("inititalize failed");
    qDebug() << "inititalize failed";
    return ;
}
else {
    qDebug() << "inititalize success";
}

// 添加當前路徑(讀檔案的時候才需要)
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')");

// 載入模組,即loadtraindata.py
PyObject *pModule = PyImport_ImportModule("kde");
if (!pModule) {
    PyErr_Print();
    qDebug() << "not loaded!";
    return ;
}
else {
    qDebug() << "load module success";
}

// 載入函式loadData()
PyObject *pLoadFunc = PyObject_GetAttrString(pModule, "loadData");

if (!pLoadFunc) {
    printf("get func failed!");
}
else {
    qDebug() << "get func success";
}

PyObject *retObjectX = PyObject_CallObject(pLoadFunc, NULL); // 獲得python指令碼返回資料

if (retObjectX == NULL) {
    qDebug() << "no return value";
    return ;
}

/*
將retObjectX匯入trainData中(二維資料)
*/
int row = PyList_Size(retObjectX);
for (int i = 0; i < row; ++i) {
    PyObject *lineObject = PyList_GetItem(retObjectX, i);
    int col = PyList_Size(lineObject);
    QVector<double> tmpVect;
    for (int j = 0; j < col; ++j) {
        PyObject *singleItem = PyList_GetItem(lineObject, j);
        double item = PyFloat_AsDouble(singleItem);
        tmpVect.push_back(item);
    }
    trainData->push_back(tmpVect);
}
Py_Finalize();

注意事項

這裡列寫一下軟體搭建過程中遇到的問題,以供參考。

  • 重灌python的話別忘了修改.pro檔案中的python路徑;
  • importError:dll not load: 常見的matplotlib,numpy等DLL載入錯誤,通常是由python與對應的第三方包的版本不一致導致的。將anaconda檔案下的python.dllpython3.dll檔案拷貝到qt可執行檔案exe同級目錄下並覆蓋。
  • 多次呼叫Py_Initialize()Py_Finalize()可能會出現異常: 最好在main.cpp裡就輸入Py_Initialize(),程式最後再Py_Finalize()

Reference:

本部落格與https://xuyunkun.com同步更新