C++調用python腳本

隨着機器學習/深度學習這幾年的的火熱,python成了當紅炸子雞,使用python訓練機器學習模型則成了開發人員們最喜歡的方法,但是由於過往調度系統一般都是用C++來開發的,因此我們只有兩種方法來調用python腳本,一種是使用上篇中提到的子進程的方法,另外一種則是直接使用C++/python進行混合編程。

基本使用方法

python 提供了一套 C API庫,使得開發者能很方便地從C/ C++ 程序中調用 Python 模塊。具體的文檔參考官方指南

至於要使用這個套API,我們需要引入一個頭文件和一個庫文件,以linux系統自帶的python 2.7爲例,python 的頭文件位於/usr/include/python2.7,庫文件爲 libpython2.7.so ,在makefile中需要增加

 -I /usr/include/python2.7 -l python2.7

我們現在知道了python api的頭文件和庫文件了,那這些接口是怎樣用的呢?下面我們按照不同的主題來拆解下。

 

初始化Python解釋器環境

python是一門解釋語言,沒有解釋器是沒法運行的。因此,我們需要爲python提供一個解釋器環境,和環境相關的接口如下

void Py_Initialize():       
    初始化python解釋器.C/C++中調用Python之前必須先初始化解釋器
int Py_IsInitialized():
    返回python解析器的是否已經初始化完成,如果已完成,返回大於0,否則返回0
void Py_Finalize() :
    撤銷Py_Initialize()和隨後使用Python/C API函數進行的所有初始化,
    並銷燬自上次調用Py_Initialize()以來創建併爲被銷燬的所有子解釋器。

調用Python腳本

在C++中要使用要調用python腳本,python api提供了幾種方式,下面拿兩個api爲例,具體的參考官方文檔這裏這裏

int PyRun_SimpleString(const char*) :
    執行一個簡單的執行python腳本命令的函數

int PyRun_SimpleFile(FILE *fp, const char *filename):
    從fp中把python腳本的內容讀取到內容中並執行,filename應該爲fp對應的文件名

舉個例子:

#include "python2.7/Python.h"

int main()
{
    Py_Initialize();    ## 初始化

    PyRun_SimpleString("print 'hello'");

    Py_Finalize();      ## 釋放資源
}

在上面例子中,我們把python代碼當作一個字符串傳給解釋器來執行,這咋一看貌似沒有什麼問題,但是,既然我們選擇了使用python,卻把python腳本寫到C++代碼裏面,這似乎有點買櫝還珠。同時,不管是字符串還是文件,都只能用於c++不需要像python傳參,同時python不會向c++返回值的情況,只執行固定腳本的場景,但是,實際的場景中是必然存在C++向python傳參,python返回結果的,這個需求下我們要怎樣做呢?別急,我們一步步來說。

動態加載python模塊並執行函數

先來說說這個問題的解法吧。python api提供了動態加載模塊並且執行函數的能力,具體會涉及到下面幾個api

//加載模塊
PyObject* PyImport_ImportModule(char *name)

PyObject* PyImport_Import(PyObject *name)
PyObject* PyString_FromString(const char*)
    上面兩個api都是用來動態加載python模塊的。區別在於前者一個使用的是C的字符串,而後者的name是一個python對象,
這個python對象需要通過PyString_FromString(const char*)來生成,其值爲要導入的模塊名


//導入函數相關
PyObject* PyModule_GetDict( PyObject *module)
    PyModule_GetDict()函數可以獲得Python模塊中的函數列表。PyModule_GetDict()函數返回一個字典。字典中的關鍵字爲函數名,值爲函數的調用地址。
字典裏面的值可以通過PyDict_GetItemString()函數來獲取,其中p是PyModule_GetDict()的字典,而key則是對應的函數名

PyObject* PyObject_GetAttrString(PyObject *o, char *attr_name)
     PyObject_GetAttrString()返回模塊對象中的attr_name屬性或函數,相當於Python中表達式語句:o.attr_name

//調用函數相關
PyObject* PyObject_CallObject( PyObject *callable_object, PyObject *args)
PyObject* PyObject_CallFunction( PyObject *callable_object, char *format, ...)
    使用上面兩個函數可以在C程序中調用Python中的函數。callable_object爲要調用的函數對象,也就是通過上述導入函數得到的函數對象,
而區別在於前者使用python的tuple來傳參,後者則使用類似c語言printf的風格進行傳參。
如果不需要參數,那麼args可能爲NULL。返回成功時調用的結果,或失敗時返回NULL。
這相當於Python表達式 apply(callable_object, args) 或 callable_object(*args)

api知道了,但是怎樣用起來呢?我們先來看一個簡單的例子。假設我們有這樣一個python腳本

#cat script/sayHello.py
def say():
	print("hello")

我們可以像下面這樣去加載模塊並且調用指定的函數

#include <python2.7/Python.h>
#include <iostream>

using namespace std;

int main(){
	Py_Initialize();
	if( !Py_IsInitialized()){
		cout << "python init fail" << endl;
		return 0;
	}
	PyRun_SimpleString("import sys");
	PyRun_SimpleString("sys.path.append('./script')");

	PyObject* pModule = PyImport_ImportModule("sayHello");
	if( pModule == NULL ){
		cout <<"module not found" << endl;
		return 1;
	}

	PyObject* pFunc = PyObject_GetAttrString(pModule, "say");
	if( !pFunc || !PyCallable_Check(pFunc)){
		cout <<"not found function add_num" << endl;
		return 0;
	}

	 PyObject_CallObject(pFunc, NULL );

	Py_Finalize();
	return 0;
}

但是,同學們應該看到,上面的函數是沒有參數而且沒有返回值的,如果有參數和返回值要怎樣做呢?

調用參數

在C/C++中,所有的Python類型都被聲明爲PyObject型,爲了能夠讓C++能夠操作python的數據,python提供了python各種數據類型和C語言數據類型的轉換操作,具體的使用方法如下

1.數字與字符串

在Python/C API中提供了Py_BuildValue()函數對數字和字符串進行轉換處理,使之變成Python中相應的數據類型。其函數原型如下所示

PyObject* Py_BuildValue( const char *format, ...)
    Py_BuildValue()提供了類似c語言printf的參數構造方法,format是要構造的參數的類型列表,函數中剩餘的參數即要轉換的C語言中的整型、浮點型或者字符串等。
其返回值爲PyObject型的指針。

format對應的類型列表如下

s(str或None)[char *]
使用'utf-8'編碼將以null結尾的C字符串轉換爲Python str對象。如果C字符串指針爲NULL,則表示None。

s#(str或None)[char *,int]
使用'utf-8'編碼將C字符串及其長度轉換爲Python str對象。如果C字符串指針爲NULL,則忽略長度返回None。

y(字節)[char *]
這會將C字符串轉換爲Python字節對象。如果C字符串指針爲NULL,則返回None。

y#(字節)[char *,int]
這會將C字符串及其長度轉換爲Python對象。如果C字符串指針爲NULL,則返回None。

z(str或None)[char *]
與s相同。

z#(str或None)[char *,int]
與s#相同。

u(str)[Py_UNICODE *]
將Unicode(UCS-2或UCS-4)數據的以null結尾的緩衝區轉換爲Python Unicode對象。如果Unicode緩衝區指針爲NULL,則返回None。

u#(str)[Py_UNICODE *,int]
將Unicode(UCS-2或UCS-4)數據緩衝區及其長度轉換爲Python Unicode對象。如果Unicode緩衝區指針爲NULL,則忽略長度並返回None。

U(str或None)[char *]
與s相同。

U#(str或None)[char *,int]
與s#相同。

i(int)[int]
將普通的C int轉換爲Python整數對象。

b(int)[char]
將純C char轉換爲Python整數對象。

h(int)[short int]
將普通的C short int轉換爲Python整數對象。

l(int)[long int]
將C long int轉換爲Python整數對象。

B(int)[unsigned char]
將C unsigned char轉換爲Python整數對象。

H(int)[unsigned short int]
將C unsigned short int轉換爲Python整數對象。

I(int)[unsigned int]
將C unsigned int轉換爲Python整數對象。

k(int)[unsigned long]
將C unsigned long轉換爲Python整數對象。

L(int)[long long]
將C long long轉換爲Python整數對象。

K(int)[unsigned long long]
將C unsigned long long轉換爲Python整數對象。

n(int)[Py_ssize_t]
將C Py_ssize_t轉換爲Python整數。

c(長度爲1的字節)[char]
將表示字節的C int轉換爲長度爲1的Python字節對象。

C(長度爲1的str)[int]
將表示字符的C int轉換爲長度爲1的Python str對象。

d(float) [double] 
將C double轉換爲Python浮點數。

f(float) [float] 
將C float轉換爲Python浮點數。

D(complex) [Py_complex *]
將C Py_complex結構轉換爲Python複數。

O(object) [PyObject *]
不改變Python對象的傳遞(引用計數除外,它增加1)。如果傳入的對象是NULL指針,則假定這是因爲產生參數的調用發現錯誤並設置了異常。
因此,Py_BuildValue()將返回NULL但不會引發異常。如果尚未引發異常,則設置SystemError。

S(object) [PyObject *]
與O相同

N((object) [PyObject *]
與O相同,但不會增加對象的引用計數。通過調用參數列表中的對象構造函數創建對象時很有用。

O&(object) [converter, anything] 
通過轉換器函數將任何內容轉換爲Python對象。該函數被調用任何東西(應與void *兼容)作爲其參數,並應返回“新”Python對象,如果發生錯誤則返回NULL。

(items) (tuple) [matching-items] 
將一系列C值轉換爲具有相同項目數的Python元組。

[items](list) [matching-items]
將一系列C值轉換爲具有相同項目數的Python列表。

{items}(dict) [matching-items] 
將一系列C值轉換爲Python字典。每對連續的C值將一個項添加到字典中,分別用作鍵和值。
如果格式字符串中存在錯誤,則設置SystemError異常並返回NULL。

2 、列表

PyObject* PyList_New( Py_ssize_t len)
    創建一個新的Python列表,len爲所創建列表的長度

int PyList_SetItem( PyObject *list, Py_ssize_t index, PyObject *item)
    向列表中添加項。當列表創建以後,可以使用PyList_SetItem()函數向列表中添加項。 list:要添加項的列表。 index:所添加項的位置索引。 item:所添加項的值。

PyObject* PyList_GetItem( PyObject *list, Py_ssize_t index)
    獲取列表中某項的值。list:要進行操作的列表。index:項的位置索引。

Py_ssize_t PyList_Size(PyObject * list)
    返回列表中列表對象的長度;這相當於列表對象上的 len(list) 。

int PyList_Append( PyObject *list, PyObject *item)
int PyList_Sort( PyObject *list)
int PyList_Reverse( PyObject *list)
    Python/C API中提供了與Python中列表操作相對應的函數。例如
列表的append方法對應於PyList_Append()函數。
列表的sort方法對應於PyList_Sort()函數。
列表的reverse方法對應於PyList_Reverse()函數。

3、元組

PyObject* PyTuple_New( Py_ssize_t len) 
    PyTuple_New()函數返回所創建的元組。其函數原型如下所示。len:所創建元組的長度。 

int PyTuple_SetItem( PyObject *p, Py_ssize_t pos, PyObject *o) 
    當元組創建以後,可以使用PyTuple_SetItem()函數向元組中添加項。p:所進行操作的元組,pos:所添加項的位置索引,o:所添加的項值。

PyObject* PyTuple_GetItem( PyObject *p, Py_ssize_t pos)
    可以使用Python/C API中PyTuple_GetItem()函數來獲取元組中某項的值。p:要進行操作的元組,pos:項的位置索引

Py_ssize_t PyTuple_Size(PyObject * p)
    獲取指向元組對象的指針,並返回該元組的大小。

int _PyTuple_Resize( PyObject **p, Py_ssize_t newsize) 
    當元組創建以後可以使用_PyTuple_Resize()函數重新調整元組的大小。其函數原型如下所示。p:指向要進行操作的元組的指針,newsize:新元組的大小

4、字典

PyObject* PyDict_New() 
    PyDict_New()函數返回所創建的字典。


int PyDict_SetItem( PyObject *p, PyObject *key, PyObject *val) 
int PyDict_SetItemString( PyObject *p, const char *key, PyObject *val) 
    當字典創建後,可以使用PyDict_SetItem()函數和PyDict_SetItemString()函數向字典中添加項。 其參數含義如下。
p:要進行操作的字典。key:添加項的關鍵字,
對於PyDict_SetItem()函數其爲PyObject型,
對於PyDict_SetItemString()函數其爲char型,val:添加項的值。 
 

PyObject* PyDict_GetItem( PyObject *p, PyObject *key) 
PyObject* PyDict_GetItemString( PyObject *p, const char *key) 
    使用Python/C API中的PyDict_GetItem()函數和PyDict_GetItemString()函數來獲取字典中某項的值。它們都返回項的值。
其參數含義如下。p:要進行操作的字典,key:添加項的關鍵字,
對於PyDict_GetItem()函數其爲PyObject型
對於PyDict_GetItemString()函數其爲char型。 

PyObject* PyDict_Items( PyObject *p) 
PyObject* PyDict_Keys( PyObject *p) 
PyObject* PyDict_Values( PyObject *p) 
    在Python/C API中提供了與Python中字典操作相對應的函數。例如
    字典的item方法對應於PyDict_Items()函數。
    字典的keys方法對應於PyDict_Keys()函數。
    字典的values方法對應於PyDict_Values()函數。
其參數p:要進行操作的字典。

但是上面所有操作都是基於C語言的,至於像c++的string以及各種容器類型,則需要自己通過封裝來實現了。例如下面這樣

// PythonParam.h
#ifndef __PYTHON_CPP_PYTHON_PARAM_H__
#define __PYTHON_CPP_PYTHON_PARAM_H__

#include <map>
#include <string>
#include <vector>

#include <python2.7/Python.h>

class PythonParamBuilder{
public:
    PythonParamBuilder();
    ~PythonParamBuilder();
    PyObject* Build();

    bool AddString( const std::string& );
    bool AddList( const std::vector<std::string>& params );
    bool AddMap( const std::map<std::string, std::string>& mapParam);
    template<typename T>
    bool AddCTypeParam( const std::string& typeStr, T v ){
		PyObject * param = Py_BuildValue( typeStr, v );
		return AddPyObject( param );
    }
private:
    bool AddPyObject( PyObject* obj );
    PyObject* ParseMapToPyDict(const std::map<std::string, std::string>& mapParam);
    PyObject* ParseListToPyList(const std::vector<std::string>& params);
    PyObject* ParseStringToPyStr( const std::string& str );
private:
    PyObject* mArgs;
    std::vector<PyObject*> mTupleObjects;
};

#endif


//PythonParam.cpp
#include "PythonParam.h"


PythonParamBuilder::PythonParamBuilder()
:mArgs(nullptr){

}

PythonParamBuilder::~PythonParamBuilder(){
    for ( std::vector<PyObject*>::iterator it = mTupleObjects.begin();
        it != mTupleObjects.end();
	 ++it ){
            if( *it )
		Py_DECREF( *it );
	}

    if ( mArgs )
        Py_DECREF(mArgs);
}

PyObject* PythonParamBuilder::Build(){
    if ( mTupleObjects.empty() )
        return nullptr;

    mArgs = PyTuple_New( mTupleObjects.size() );

    for ( int i=0; i<mTupleObjects.size(); i++){
         PyTuple_SetItem(mArgs, i, mTupleObjects[i] );
    }

    return mArgs;
}

bool PythonParamBuilder::AddPyObject( PyObject* obj ){
     if ( nullptr == obj )
        return false;
     mTupleObjects.push_back(obj);
     return true;
}

bool PythonParamBuilder::AddString( const std::string& str ){
     return AddPyObject( ParseStringToPyStr( str ) );
}

bool PythonParamBuilder::AddList( const std::vector<std::string>& params ){
    return AddPyObject( ParseListToPyList(params) );
}

bool PythonParamBuilder::AddMap( const std::map<std::string, std::string>& mapParam){
    return AddPyObject( ParseMapToPyDict( mapParam ) );
}

PyObject* PythonParamBuilder::ParseMapToPyDict(const std::map<std::string, std::string>& mapParam){
    PyObject* pyDict = PyDict_New();
    for (std::map<std::string, std::string>::const_iterator it = mapParam.begin();
          it != mapParam.end();
          ++it) {
              PyObject* pyValue = PyString_FromString(it->second.c_str());
              if (pyValue == NULL) {
                   printf( "Parse param:[%s] to PyStringObject failed.", it->second.c_str());
		   return NULL;
	      }
	      if (PyDict_SetItemString(pyDict, it->first.c_str(), pyValue) < 0) {
		   printf( "Parse key:[%s] value:[%s] failed.", it->first.c_str(), it->second.c_str());
		   return NULL;
	      }
    } 
    return pyDict;
}

PyObject* PythonParamBuilder::ParseListToPyList(const std::vector<std::string>& params){
    size_t size = params.size();
    PyObject* paramList =  PyList_New(size);
    for (size_t i = 0; i < size; i++) {
	PyObject* pyStr = PyString_FromStringAndSize(params[i].data(), params[i].size());
	if (pyStr == NULL) {
  	    printf( "Parse param:[%s] to PyStringObject failed.", params[i].c_str());
	    break;
	}
	if (PyList_SetItem(paramList, i, pyStr) != 0) {
	    printf( "param:[%s] append to PyParamList failed.", params[i].c_str());
	    break;
	}
    }
    return paramList;
}

PyObject* PythonParamBuilder::ParseStringToPyStr( const std::string& str ){
    return PyString_FromStringAndSize( str.data(), str.size() );
}

返回值

python函數的返回值也是PyObject類型,因此,在python腳本返回到C/C++之後,需要解構Python數據爲C的類型,這樣C/C++程序中才可以使用Python裏的數據。但是,由於python的返回值有多種數據結構類型,因此,我們需要爲每個類型進行轉換。不過由於篇幅問題,我們只是介紹簡單的整形和字符串類型的處理,其他類型的返回見文末的github鏈接,總體思路都是根據類型逐個從值從PyObject中提取。python提供了下面函數來完成這個功能

int PyArg_Parse( PyObject *args, char *format, ...)
     根據format把args的值轉換成c類型的值,format接受的類型和上述Py_BuildValue()的是一樣的

釋放資源
Python使用引用計數機制對內存進行管理,實現自動垃圾回收。在C/C++中使用Python對象時,應正確地處理引用計數,否則容易導致內存泄漏。在Python/C API中提供了Py_CLEAR()、Py_DECREF()等宏來對引用計數進行操作。
當使用Python/C API中的函數創建列表、元組、字典等後,就在內存中生成了這些對象的引用計數。在對其完成操作後應該使用Py_CLEAR()、Py_DECREF()等宏來銷燬這些對象。其原型分別如下所示

void Py_CLEAR(PyObject *o)
void Py_DECREF(PyObject *o)
其中,o的含義是要進行操作的對象。
對於Py_CLEAR()其參數可以爲NULL指針,此時,Py_CLEAR()不進行任何操作。而對於Py_DECREF()其參數不能爲NULL指針,否則將導致錯誤。

好了,把各個細節都說了一遍了,下面是另外一個簡單的例子,在這個例子中,我們會有輸入參數和返回值

/*
cat script/Py2Cpp.py

def add_num(a,b):
	return a+b

*/

#include <python2.7/Python.h>
#include <iostream>

using namespace std;

int main(){
	Py_Initialize();
	if( !Py_IsInitialized()){
		cout << "python init fail" << endl;
		return 0;
	}
	PyRun_SimpleString("import sys");
	PyRun_SimpleString("sys.path.append('./script')");


	PyObject* moduleName = PyString_FromString("Py2Cpp");
	PyObject* pModule = PyImport_Import(moduleName);
	if( pModule == NULL ){
		cout <<"module not found" << endl;
		return 1;
	}

	PyObject* pFunc = PyObject_GetAttrString(pModule, "add_num");
	if( !pFunc || !PyCallable_Check(pFunc)){
		cout <<"not found function add_num" << endl;
		return 0;
	}

	PyObject* args = Py_BuildValue("(ii)", 28, 103);
	PyObject* pRet = PyObject_CallObject(pFunc, args );
        Py_DECREF(args);

	int res = 0;
	PyArg_Parse(pRet, "i", &res );
        Py_DECREF(pRet);
	cout << res << endl;

	Py_Finalize();
	return 0;
}

one more thing

C++調用python的過程中,好多地方都需要用到資源的釋放,包括解釋器的創建和銷燬,資源的計數減少等等,這些可以藉助C++的RAII來把事情做的更好,具體的封裝類見這裏。

 

參考材料:

Python 2.7.15 中文文檔 - 1. 用C或C擴展Python | Docs4devwww.docs4dev.com

 

Python / C API參考手冊www.docs4dev.com

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章