Python + C/C++ 嵌入式編程 (基礎)

Python 提供了豐富的 C API 函數,我們使用這些 C API 函數可以實現將 Python 文件中的函數、類等在 C/C++ 文件中進行調用,從而使得我們可以方便地使用 Python 代碼來幫助我們實現一些額外的需求(如:嵌入神經網絡模型)。

網上已經有很多介紹如何將 Python 嵌入到 C/C++ 的博客,這裏不再累述。這裏主要敘述一下如何實現多維數組在 Python 文件和 C/C++文件間互傳,即如何從 Python 文件中返回 Numpy 數組,已經如何從 C/C++文件中傳遞一個多維數組到Python文件中。在處理這個的過程中,遇到了許多的困難,查閱了許多網站,發現中文博客中對這部分的相信介紹不是很多,故特此寫這篇論文(第一篇博文,不詳細的地方望評論中指出,有問題也歡迎在評論中諮詢)。

博文目錄:

  • 從 Python 文件中的函數返回 List 數組
  • 從 Python 文件中的函數返回包含 Numpy Array 的 List 數組
  • 向 Python 文件中的函數傳遞 List 數組
  • 向 Python 文件中的函數傳遞 Numpy Array 數組

從 Python 文件中的函數返回 List 數組:

如果我們在 C/C++ 文件中調用的 Python 函數返回的是 List 數組,那麼我們這裏主要用到的是 Python C API 中的 List Object 來處理返回的數據,主要用到 List Object 裏面的這些函數:

  • int PyList_Check(PyObject *list)函數判斷一個 PyObject 指針對象是不是一個 List 對象;
  • Py_ssize_t PyList_Size (PyObject *list) 函數計算一個 List 對象的大小;
  • PyObject* PyList_GetItem(PyObject *list, Py_ssize_t index) 函數返回 List對象中第 index 個元素的 PyObject 指針。

示例:假設我們有這麼一個Python函數:

def IntegerListReturn():

    IntegerList = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

    return IntegerList

我們在 C/C++ 文件中調用這個 Python 函數時,將返回 List 對象,那麼我們將在 C/C++文件中進行如下的接收操作:

 // some code omitted...
 cout<<"Integer List Show:"<<endl;
 PyObject *pFuncTwo     = PyDict_GetItemString(pDict, "IntegerListReturn");
 PyObject *FuncTwoBack  = PyObject_CallObject (pFuncTwo, nullptr);//返回List對象

 if(PyList_Check(FuncTwoBack)){ //檢查是否爲List對象

     int SizeOfList = PyList_Size(FuncTwoBack);//List對象的大小,這裏SizeOfList = 3
     for(Index_i = 0; Index_i < SizeOfList; Index_i++){

         PyObject *ListItem = PyList_GetItem(FuncTwoBack, Index_i);//獲取List對象中的每一個元素

         int NumOfItems = PyList_Size(ListItem);//List對象子元素的大小,這裏NumOfItems = 3

         for(Index_k = 0; Index_k < NumOfItems; Index_k++){

             PyObject *Item = PyList_GetItem(ListItem, Index_k);//遍歷List對象中子元素中的每個元素

             cout << PyInt_AsLong(Item) <<" "; //輸出元素

             Py_DECREF(Item); //釋放空間
         }

         Py_DECREF(ListItem); //釋放空間
     }
     cout<<endl;

 }else{

     cout<<"Not a List"<<endl;
 }
 // some code omitted...

從 Python 文件中的函數返回包含 Numpy Array 的 List 數組

帶有 Numpy Array 的 List 數組,除了上述提到的 List Object 操作函數,這裏還需要用到 PyArrayObject 這個對象來處理返回的 Numpy Array。先介紹一下PyArrayObject, 這個 C API 模塊來自 Numpy Module 中,所以使用這個 C API 模塊前需要進行一些初始化操作:

// some code omitted...
#include <numpy/arrayobject.h> //包含 numpy 中的頭文件arrayobject.h
using namespace std;

void init_numpy(){//初始化 numpy 執行環境,主要是導入包,python2.7用void返回類型,python3.0以上用int返回類型

    import_array();
}
int main()
{

    Py_Initialize();
    init_numpy();
    // some code omitted...

做完初始化後,我們就可以使用 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, j]位置處的元素的值
 PyArrayObject *array
 array->data + i*array->strides[0] + j*array->strides[1]

知道如何訪問PyArrayObject對象中的數據後,這裏給出簡單的示例:

#假設我們有這麼一段 python 代碼:
def ArrayListReturn():

    ArrList = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

    Array_A  = np.asarray(ArrList, dtype='float' )
    Array_B  = np.asarray(ArrList, dtype='double')

    return [Array_A, Array_B]

那麼我們在 C/C++中做如下的訪問:

/*Return the List which contains Numpy Array*/
PyObject *pFuncOne    = PyDict_GetItemString(pDict, "ArrayListReturn");

PyObject *FuncOneBack = PyObject_CallObject(pFuncOne, nullptr);

int Index_i = 0, Index_k = 0, Index_m = 0, Index_n = 0;
if(PyList_Check(FuncOneBack)){

    int SizeOfList = PyList_Size(FuncOneBack);

    for(Index_i = 0; Index_i < SizeOfList; Index_i++){

        PyArrayObject *ListItem = (PyArrayObject *)PyList_GetItem(FuncOneBack, Index_i);//讀取List中的PyArrayObject對象,這裏需要進行強制轉換。

        int Rows = ListItem->dimensions[0], columns = ListItem->dimensions[1];
        cout<<"The "<<Index_i<<"th Array is:"<<endl;
        for(Index_m = 0; Index_m < Rows; Index_m++){

            for(Index_n = 0; Index_n < columns; Index_n++){

                cout<<*(double *)(ListItem->data + Index_m * ListItem->strides[0] + Index_n * ListItem->strides[1])<<" ";//訪問數據,Index_m 和 Index_n 分別是數組元素的座標,乘上相應維度的步長,即可以訪問數組元素
            }
            cout<<endl;
        }

        Py_DECREF(ListItem);
    }
}else{

    cout<<"Not a List"<<endl;
}

向 Python 文件中的函數傳遞 List 數組

現在我們的需求是我們要將 C/C++文件中的數組傳遞給 Python 文件的某個函數,那麼我們將藉助 List Object 和 Tuple Object 來封裝我們的數據,從而傳遞給 Python 文件中的函數。

#假設現在我們有這樣一個Python函數,其功能是接受一個由 C/C++ 文件傳遞的List數組,並打印出來
def PassListFromCToPython(List):

    PyList = List
    print (PyList)

那麼在 C/C++ 文件端,我們需要做如下處理:

/*Pass by List: Transform an C Array to Python List*/

    double CArray[] = {1.2, 4.5, 6.7, 8.9, 1.5, 0.5};

    PyObject *PyList  = PyList_New(6);//定義一個與數組等長的PyList對象數組
    PyObject *ArgList = PyTuple_New(1);//定義一個Tuple對象,Tuple對象的長度與Python函數參數個數一直,上面Python參數個數爲1,所以這裏給的長度爲1
    for(Index_i = 0; Index_i < PyList_Size(PyList); Index_i++){

        PyList_SetItem(PyList, Index_i, PyFloat_FromDouble(CArray[Index_i]));//給PyList對象的每個元素賦值
    }

    PyObject *pFuncFour = PyDict_GetItemString(pDict, "PassListFromCToPython");
    cout<<"C Array Pass Into The Python List:"<<endl;
    PyTuple_SetItem(ArgList, 0, PyList);//將PyList對象放入PyTuple對象中
    PyObject_CallObject(pFuncFour, ArgList);//調用函數,完成傳遞

向 Python 文件中的函數傳遞 Numpy Array 數組

當我們需要向 Python 文件中傳遞一個多維數組時,這個時候我們藉助PyArrayObject 和 PyTuple 會更加的合適。

#假設現在我們的Python函數是接受一個 Numpy Array 數組進行處理
def PassArrayFromCToPython(Array):

    print "Shape Of Array:", Array.shape

    print Array

那麼我們就需要在 C/C++ 文件中做如下的處理:

double CArrays[3][3] = {{1.3, 2.4, 5.6}, {4.5, 7.8, 8.9}, {1.7, 0.4, 0.8}}; //定義一個3 X 3的數組

    npy_intp Dims[2] = {3, 3}; //給定維度信息

    PyObject *PyArray  = PyArray_SimpleNewFromData(2, Dims, NPY_DOUBLE, CArrays); //生成包含這個多維數組的PyObject對象,使用PyArray_SimpleNewFromData函數,第一個參數2表示維度,第二個爲維度數組Dims,第三個參數指出數組的類型,第四個參數爲數組
    PyObject *ArgArray = PyTuple_New(1);
    PyTuple_SetItem(ArgArray, 0, PyArray); //同樣定義大小與Python函數參數個數一致的PyTuple對象

    PyObject *pFuncFive = PyDict_GetItemString(pDict, "PassArrayFromCToPython");
    PyObject_CallObject(pFuncFive, ArgArray);//調用函數,傳入Numpy Array 對象。

代碼塊

下面給出完整的代碼示例:

#ModuleOne.py文件
def ArrayListReturn():

    ArrList = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

    Array_A  = np.asarray(ArrList, dtype='float' )
    Array_B  = np.asarray(ArrList, dtype='double')

    return [Array_A, Array_B]

def IntegerListReturn():

    IntegerList = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

    return IntegerList

def FloatListReturn():

    FloatList = [[1.2, 2.3, 3.5], [0.5, 5.2, 6.5], [7.2, 8.8, 9.3]]

    return FloatList

def PassListFromCToPython(List):

    PyList = List
    print (PyList)

def PassArrayFromCToPython(Array):

    print "Shape Of Array:", Array.shape

    print Array

C/C++文件:

#include <iostream>
#include <Python.h>
#include <numpy/arrayobject.h>
using namespace std;

void init_numpy(){

    import_array();
}
int main()
{

    Py_Initialize();
    init_numpy();

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('/home/liaojian/Documents/Programming/PythonWorkSpace/CalledByCplusplus/')");

    PyObject *pModule  = nullptr;
    PyObject *pDict    = nullptr;

    pModule  = PyImport_ImportModule("ModuleOne");
    pDict    = PyModule_GetDict(pModule);

    /*Return the List which contains Numpy Array*/
    PyObject *pFuncOne    = PyDict_GetItemString(pDict, "ArrayListReturn");

    PyObject *FuncOneBack = PyObject_CallObject(pFuncOne, nullptr);

    int Index_i = 0, Index_k = 0, Index_m = 0, Index_n = 0;
    if(PyList_Check(FuncOneBack)){

        int SizeOfList = PyList_Size(FuncOneBack);

        for(Index_i = 0; Index_i < SizeOfList; Index_i++){

            PyArrayObject *ListItem = (PyArrayObject *)PyList_GetItem(FuncOneBack, Index_i);

            int Rows = ListItem->dimensions[0], columns = ListItem->dimensions[1];
            cout<<"The "<<Index_i<<"th Array is:"<<endl;
            for(Index_m = 0; Index_m < Rows; Index_m++){

                for(Index_n = 0; Index_n < columns; Index_n++){

                    cout<<*(double *)(ListItem->data + Index_m * ListItem->strides[0] + Index_n * ListItem->strides[1])<<" ";
                }
                cout<<endl;
            }

            Py_DECREF(ListItem);
        }
    }else{

        cout<<"Not a List"<<endl;
    }

    /*Return Integer List and Access to Each Items*/

    cout<<"Integer List Show:"<<endl;
    PyObject *pFuncTwo     = PyDict_GetItemString(pDict, "IntegerListReturn");
    PyObject *FuncTwoBack  = PyObject_CallObject (pFuncTwo, nullptr);

    if(PyList_Check(FuncTwoBack)){

        int SizeOfList = PyList_Size(FuncTwoBack);
        for(Index_i = 0; Index_i < SizeOfList; Index_i++){

            PyObject *ListItem = PyList_GetItem(FuncTwoBack, Index_i);

            int NumOfItems = PyList_Size(ListItem);

            for(Index_k = 0; Index_k < NumOfItems; Index_k++){

                PyObject *Item = PyList_GetItem(ListItem, Index_k);

                cout << PyInt_AsLong(Item) <<" ";

                Py_DECREF(Item);
            }

            Py_DECREF(ListItem);
        }
        cout<<endl;


    }else{

        cout<<"Not a List"<<endl;
    }

    /*Return Float List and Access to Each Items*/

    cout<<"Double List Show:"<<endl;
    PyObject *pFunThree     = PyDict_GetItemString(pDict, "FloatListReturn");
    PyObject *FuncThreeBack = PyObject_CallObject  (pFunThree, nullptr);

    if(PyList_Check(FuncThreeBack)){

        int SizeOfList = PyList_Size(FuncThreeBack);
        for(Index_i = 0; Index_i < SizeOfList; Index_i ++){

            PyObject *ListItem = PyList_GetItem(FuncThreeBack, Index_i);
            int NumOfItems = PyList_Size(ListItem);

            for(Index_k = 0; Index_k < NumOfItems; Index_k++){

                PyObject *Item  = PyList_GetItem(ListItem, Index_k);

                cout<< PyFloat_AsDouble(Item) << " ";

                Py_DECREF(Item);
            }

            Py_DECREF(ListItem);
        }
        cout<<endl;


    }else{

        cout<<"Not a List"<<endl;
    }

    /*Pass by List: Transform an C Array to Python List*/

    double CArray[] = {1.2, 4.5, 6.7, 8.9, 1.5, 0.5};

    PyObject *PyList  = PyList_New(6);
    PyObject *ArgList = PyTuple_New(1);
    for(Index_i = 0; Index_i < PyList_Size(PyList); Index_i++){

        PyList_SetItem(PyList, Index_i, PyFloat_FromDouble(CArray[Index_i]));
    }

    PyObject *pFuncFour = PyDict_GetItemString(pDict, "PassListFromCToPython");
    cout<<"C Array Pass Into The Python List:"<<endl;
    PyTuple_SetItem(ArgList, 0, PyList);
    PyObject_CallObject(pFuncFour, ArgList);

    /*Pass by Python Array: Transform an C Array to Python Array*/

    double CArrays[3][3] = {{1.3, 2.4, 5.6}, {4.5, 7.8, 8.9}, {1.7, 0.4, 0.8}};

    npy_intp Dims[2] = {3, 3};

    PyObject *PyArray  = PyArray_SimpleNewFromData(2, Dims, NPY_DOUBLE, CArrays);
    PyObject *ArgArray = PyTuple_New(1);
    PyTuple_SetItem(ArgArray, 0, PyArray);

    PyObject *pFuncFive = PyDict_GetItemString(pDict, "PassArrayFromCToPython");
    PyObject_CallObject(pFuncFive, ArgArray);



    //Release

    Py_DECREF(pModule);
    Py_DECREF(pDict);
    Py_DECREF(FuncOneBack);
    Py_DECREF(FuncTwoBack);
    Py_DECREF(FuncThreeBack);
    Py_DECREF(PyList);
    Py_DECREF(ArgList);
    Py_DECREF(PyArray);
    Py_DECREF(ArgArray);
    return 0;
}

執行結果: 

這是執行C / C++文件後得到的結果

上面是嵌入式編程的基礎,C++中調用python時,傳遞大型數組是一個大問題,這裏使用 boost.python (linux)

  • 下載C++的boost庫:boost.org/
  • 解壓boost文件,在其目錄中執行./bootstrap.sh --with-libraries=all --with-toolset=gcc,會生成編譯器b2bjam
  • 修改project-config.jam文件,加入python的版本及路徑(不加入則會默認python2.7):

    說明: 如果使用Anaconda 虛擬環境 ,最好修改 .bashrc 文件內容,例如:


這裏就會去我的虛擬環境 tensorflow-keras 下去找到 python,不然默認會找到python2.7

編譯boost

執行以下命令開始進行boost的編譯:

./b2 toolset=gcc
安裝boost

最後執行以下命令開始安裝boost:

./b2 install --prefix=/usr

--prefix=/usr用來指定boost的安裝目錄,不加此參數的話默認的頭文件在/usr/local/include/boost目錄下,庫文件在/usr/local/lib/目錄下。這裏把安裝目錄指定爲--prefix=/usr則boost會直接安裝到系統頭文件目錄和庫文件目錄下,可以省略配置環境變量

C++代碼:

#include<boost/python.hpp>
#include<boost/python/numpy.hpp>
#include<iostream>

namespace p = boost::python;
namespace np = boost::python::numpy;

int main(int argc, char *argv[]){
    \\初始化python解釋器
    Py_Initialize();
    \\導入python模塊
    p::object pModule = p::import("mine");
    \\導入python函數
    p::object func1 = pModule.attr("func1");

    \\初始化numpy
    np::initialize();
    \\生成ndarray類實例
    uint8_t data_in_c[] = {1,2,3,4,5,6,7,8,1,3,5,7};
    p::tuple shape = p::make_tuple(3, 4);
    p::tuple stride = p::make_tuple(4, 1);
    np::dtype dt1 = np::dtype::get_builtin<uint8_t>();
    np::ndarray data_from_c = np::from_data(data_in_c, dt1,
        shape, stride, p::object());

    \\在C++中輸出ndarray
    std::cout << "C++ ndarray:" << std::endl;
    std::cout << p::extract<char const *>(p::str(data_from_c)) << std::endl;
    std::cout << std::endl;

    \\調用python的函數,並傳入ndarray,之後取回結果
    p::object data_obj_from_python = func1(data_from_c);;
    \\返回值是p::object類型,轉換爲np::array類型
    np::ndarray data = np::array(data_obj_from_python);
    \\取出ndarry的數據指針
    \\由於某種原因,data.get_data()得到的數據指針是char *類型,需要轉換爲對應的數據類型
    \\詳見:https://github.com/boostorg/python/blob/develop/include/boost/python/numpy/ndarray.hpp#L41-L55
    double *pp = reinterpret_cast<double*> (data.get_data());

    \\調用python的函數,並傳入ndarray
    std::cout << "data from python:" << std::endl;
    std::cout << p::extract<char const *>(p::str(data)) << std::endl;
    std::cout << std::endl;

    std::cout << "pointer:" << std::endl;
    for (int i = 0; i < 9; i++) {
    	std::cout << *(pp+i)  << std::endl;
    }
}

Python代碼mine.py(需要跟編譯後的exe文件放在同一個目錄下):

import numpy as np
data = np.random.random((3,3))

def func1(data_from_C):
    print("data_from_C++.shape:")
    print(data_from_C.shape)
    print('')
    print("data_from_C++:")
    print(data_from_C)
    print('')

    print("data.type:")
    print(data.dtype)
    print('')
    print("data in python")
    print(data)
    print('')

    return data

輸出結果:

C++ ndarray:
[[1 2 3 4]
 [5 6 7 8]
 [1 3 5 7]]

data_from_C++.shape:
(3, 4)

data_from_C++:
[[1 2 3 4]
 [5 6 7 8]
 [1 3 5 7]]

data.type:
float64

data in python
[[ 0.70759695  0.39755579  0.9951812 ]
 [ 0.97369017  0.57502282  0.25125566]
 [ 0.92008613  0.74213496  0.64438868]]

data from python:
[[ 0.70759695  0.39755579  0.9951812 ]
 [ 0.97369017  0.57502282  0.25125566]
 [ 0.92008613  0.74213496  0.64438868]]

pointer:
0.707597
0.397556
0.995181
0.97369
0.575023
0.251256
0.920086
0.742135
0.644389

該方法爲指針傳值,python中的ndarray與C++中的數組共享數據空間。調用及返回的開銷極低,完全可以忽略不計,而且使用方法比目前網上所有的方法都要簡單。

注意:運行時如果出現錯誤:

Fatal Python error: Py_Initialize: unable to load the file system codec. 
ImportError: No module named 'encodings'

則需要把 stage\lib 加入環境系統環境變量 PATH,


問題:編譯boost 沒有boost-python 動態庫和靜態庫

這裏在編譯boost 時,要保證系統安裝python-dev, 這相當於python的擴展庫!!!


參考網站:

  1. List Object 對象介紹:https://docs.python.org/2.7/c-api/list.html?highlight=pylist_new#c.PyList_New
  2. Tuple Object 對象介紹:https://docs.python.org/2.7/c-api/tuple.html?highlight=pytuple_new
  3. PyArrayObject對象介紹:https://docs.scipy.org/doc/numpy-1.10.0/reference/c-api.array.html#c.import_array
  4. PyArrayObject對象使用介紹:http://folk.uio.no/hpl/scripting/doc/python/NumPy/Numeric/numpy-13.html
  5. Python與C /C++嵌入式編程:https://www.codeproject.com/Articles/11805/Embedding-Python-in-C-C-Part-I
  6. boost 編譯安裝:https://blog.csdn.net/this_capslock/article/details/47170313
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章