隨着機器學習/深度學習這幾年的的火熱,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