boost.python筆記
標簽:boost.python
簡介
Boost.python是什么尘惧?
它是boost庫的一部分,隨boost一起安裝含衔,用來實現C++和Python代碼的交互股耽。
使用Boost.python有什么特點颜曾?
不需要修改原有的C++代碼泽示,支持比較豐富的C++特性缸血。不會生成額外的python代碼(像SWIG那樣),但是需要寫一部分C++的封裝代碼械筛。
我只用到了其功能的一部分捎泻,把C/C++實現的功能封裝為可供python直接調用的.so庫。具體場景是变姨,有一個C++模塊通過thrift封裝為RPC族扰,python代碼通過PRC調用請求服務。由于調用頻次較多定欧,RPC調用開銷成為一個很耗時的部分,因此想直接通過python對原模塊功能進行調用怒竿。
以前了解SWIG可以實現這個需求砍鸠。本已開始看SWIG的文檔,但突然又想看一下還有沒有別的方法耕驰,于是在stack overflow上搜了一些問題爷辱,發(fā)現不少人推薦Boost.python,于是打算拿它來試一試。
如何使用Boost.python
首先饭弓,前提是安裝了開發(fā)環(huán)境双饥。
- 安裝了boost開發(fā)環(huán)境。安裝了頭文件和動態(tài)庫弟断。
- 安裝了python開發(fā)環(huán)境咏花。安裝了頭文件和動態(tài)庫。
然后阀趴,就是寫代碼了昏翰,這是個沒辦法避免的事情!A跫薄棚菊!
需要自己動手的有兩個地方。一個是xxxxxx_wrapper.cpp文件叔汁,文件名無所謂统求,其核心目的是定義導出的python模塊的名稱,以及需要導出的類据块、函數等码邻。另一個是需要修改一下你的Makefile,來編譯瑰钮、鏈接這個so庫冒滩。
先來看一下xxxxxx_wrapper.cpp。一般情況下浪谴,它的內容跟下面的代碼比較接近开睡。
#include <boost/python.hpp>
// 其他需要包含的頭文件,與具體業(yè)務有關
namespace py = boost::python;
// 其他函數苟耻,可能包括一些用于類型轉換和封裝的
BOOST_PYTHON_MODULE(my_module_name)
{
// 導出普通函數
def("fun_name_in_python", &fun_name_in_c);
// 導出類及部分成員
class_<ClassNameInCpp>("ClassNameInPython", init<std::string>()) //類名篇恒,默認構造函數
.def(init<double>()) //其他構造函數
.def("memberFunNameInPython", &ClassNameInCpp::memberFunNameInCpp) //成員函數
.def_readwrite("dataMemberInPython", &ClassNameInCpp::dataMemberInCpp) //普通成員變量
.def_readonly("dataMemberInPython_2", &ClassNameInCpp::dataMemberInCpp_2) //只讀成員變量
;
}
這個文件的核心目的體現在BOOST_PYTHON_MODULE
里,定義需要導出給python的東西凶杖。
在Makefile里胁艰,需要增加一條用來編譯導出的.so的規(guī)則,編譯命令里通用的部分一般像下面這樣智蝠,這里把編譯和鏈接寫在一起了腾么。
g++ -o my_module_name.so -shared -fPIC -I${BOOST_INCLUDE_PATH} -I${PYTHON_INCLUDE_PATH} -L${BOOST_LIB_DIR} -lboost_python ${MY_SRC_FILES}
編譯及鏈接參數的作用如下,其他參數由具體項目的業(yè)務邏輯決定:
-o my_module_name.so
杈湾,這里的模塊名需要和xxxxxx_wrapper.cpp文件里BOOST_PYTHON_MODULE(my_module_name)
一致解虱;-I${BOOST_INCLUDE_PATH} -I${PYTHON_INCLUDE_PATH}
是編譯需要的;-L${BOOST_LIB_DIR} -lboost_python -shared -fPIC
是鏈接需要的漆撞;${MY_SRC_FILES}
包含了xxxxxxx_wrapper.cpp以及業(yè)務邏輯需要的其他.cpp殴泰,.c文件于宙;
至此,我們便有了my_module_name.so這個可以被python調用的模塊了悍汛。測試一下吧捞魁。
>>>import my_moduel_name
>>>help(my_module_name)
可以看到被導出的類及函數,然后可以按照python的習慣來使用這些類和函數了离咐。
如何寫wrapper
以一個實例為框架來解釋吧谱俭,內容包括普通函數、類健霹、數據成員旺上、成員函數、通過參數傳遞結果糖埋、容器宣吱。其他特性沒有用到,也沒有測試瞳别。
先來看一下業(yè)務邏輯的代碼征候。包含一個類,一個以類對象為參數的函數祟敛,一個通過引用修改類對象的函數疤坝。
//test_class.h
// 定義一個類
class A
{
public:
A(){privateVal=0;} //默認構造函數
A(int val){privateVal=val;} //帶參數的構造函數
void set(int val){privateVal=val;} //成員函數
int get() const {return privateVal;}; //成員函數
int publicVal; //公共數據成員
private:
int privateVal; //私有數據成員
};
int addA(A &a, int addVal); //普通函數,有返回值馆铁,通過引用修改參數
void printA(const A& a);
//test_class.cpp
#include <stdio.h>
#include "test_class.h"
int addA(A &a, int addVal)
{
int val = a.get();
val += addVal;
a.set(val);
return val;
}
void printA(const A& a)
{
printf("%d\n", a.get());
}
然后是wrapper.cpp文件跑揉,這里實際名為test_class_wrapper.cpp。
//test_class_wrapper.cpp
#include <boost/python.hpp>
#include "test_class.h"
BOOST_PYTHON_MODULE(test_class)
{
using namespace boost::python;
// 導出類
class_<A>("A", init<>()) //如果默認構造函數沒有參數埠巨,可以省略
.def(init<int>()) //其他構造函數
.def("get", &A::get) //成員函數
.def("set", &A::set) //成員函數
.def_readwrite("publicVal", &A::publicVal) //數據成員历谍,當然是公共的
;
def("printA", &printA);
def("addA", &addA);
}
通過python命令行測試一下
>>>import test_class
>>>a = test_class.A(5)
>>>ret = addA(a, 10)
>>>print ret
15
>>>print a.get()
15
到目前為止,整個過程都很順利辣垒。需要額外寫的代碼很少望侈,也很規(guī)整,與某種IDL的寫法接近勋桶,只需要“聲明”一下脱衙,剩下的事情都交給編譯器及庫完成。但有的時候例驹,這個過程就不這么順利了捐韩,我們需要額外寫一些轉換及封裝。比如在這一部分最開始提到的容器鹃锈,上面的代碼就沒有涉及奥帘。
下面的代碼,我們對上面的例子做了一些擴展仪召。第一寨蹋,對類A增加了一個vector成員,需要在python代碼里引用該成員扔茅;第二已旧,增加了一個函數,以vector為參數召娜,需要在python代碼里直接調用該函數运褪。下面我們就來解釋與容器有關的導出。
#include<vector>
class B;
class A
{
public:
A(){privateVal=0;} //默認構造函數
A(int val){privateVal=val;} //帶參數的構造函數
void set(int val){privateVal=val;} //成員函數
int get() const {return privateVal;}; //成員函數
int publicVal; //公共數據成員
std::vector<B> m_vB;
private:
int privateVal; //私有數據成員
};
class B
{
public:
B(){}
~B(){}
int pos;
int len;
};
int accumulate(const std::vector<A>& v_A);
int addA(A &a, int addVal); //普通函數玖瘸,有返回值秸讹,通過引用修改參數
void printA(const A& a);
#include <stdio.h>
#include "test_class.h"
int addA(A &a, int addVal)
{
int val = a.get();
val += addVal;
a.set(val);
return val;
}
void printA(const A& a)
{
printf("%d\n", a.get());
}
int accumulate(const std::vector<A>& v_A)
{
int ret = 0;
for (size_t i = 0; i < v_A.size(); i++)
{
ret += v_A[i].get();
}
return ret;
}
首先,需要明白一點雅倒,c++中的vector不等于python中的list璃诀,雖然它們看上去比較相似。Boost.python中有與python的list對應的東西蔑匣,是boost::python::list劣欢,如果在python代碼里以list為參數調用某個方法,則在c++代碼中這個參數被自動映射為boost::python::list裁良,不是vector凿将。既然這樣,如果我們不打算修改原有的C++代碼价脾,又想調用以vector為參數的函數牧抵,該怎么辦呢?
目前我了解的方法由兩種:
- 在C++代碼里對以vector為參數的函數進行一層封裝侨把,封裝為以boost::python::list為參數的函數犀变,導出封裝后的函數。在函數里通過
boost::python::extract_<T>
對list里的所有成員進行提取座硕,將其由boost::python::object對象變?yōu)門類型的對象弛作,然后存于vector<T>
中,再調用以vector<T>
為參數的函數华匾。
如果需要返回list或者原函數對vector參數的內容作了修改映琳,需要再將調用函數后的vector內的每個元素放回list里。- 直接導出
vector<T>
類型蜘拉。此時vector<T>
本身作為一個類型被導出給python代碼萨西,與普通的類具有同等地位。但是旭旭,與普通類不同的是谎脯,它通過模板vector_indexing_suite<std::vector<T> >()
導出,自動實現了append持寄,slice源梭,__len__
等方法娱俺,在python里可以像使用list那樣操作這個被導出的vector類。而且废麻,以vector<T>
為參數的函數荠卷,在通過def導出給python時,其參數會被自動映射為vector_indexing_suite<std::vector<T> >
烛愧。同理油宜,python代碼傳入的通過vector_indexing_suite
導出的容器對象,也會在c++代碼里被自動轉換為vector怜姿,這里無需顯式地寫轉換函數慎冤。
下面的代碼是wrapper文件,使用第二種方法沧卢,即直接導出vector類型蚁堤。
為什么這么做呢?因為A里有個vector成員搏恤,要導出這個成員违寿,必須導出這個vector<B>
這個類型,否則還需要對類A再做一層封裝熟空,讓它包含一個boost::python::list
成員藤巢,這就太麻煩了。
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
#include "test_class.h"
bool operator==(const B& left, const B& right);
bool operator==(const A& left, const A& right)
{
if (left.get() != right.get() || left.publicVal != right.publicVal)
return false;
if (left.m_vB.size() != right.m_vB.size())
return false;
for (size_t i = 0; i < left.m_vB.size(); i ++)
{
if (!(left.m_vB[i] == right.m_vB[i]))
return false;
}
return true;
}
bool operator==(const B& left, const B& right)
{
return (left.pos == right.pos && left.len == right.len);
}
BOOST_PYTHON_MODULE(test_class)
{
using namespace boost::python;
class_<A>("A", init<>()) //如果默認構造函數沒有參數息罗,可以省略
.def(init<int>()) //其他構造函數
.def("get", &A::get) //成員函數
.def("set", &A::set) //成員函數
.def_readwrite("publicVal", &A::publicVal) //數據成員掂咒,當然是公共的
.def_readwrite("vB", &A::m_vB)
;
class_<std::vector<A> >("VecA")
.def(vector_indexing_suite<std::vector<A> >())
;
class_<B>("B")
.def_readwrite("pos", &B::pos)
.def_readwrite("len", &B::len)
;
class_<std::vector<B> >("VecB")
.def(vector_indexing_suite<std::vector<B> >())
;
def("printA", &printA);
def("addA", &addA);
def("accumulate", &accumulate);
}
我們還是從BOOST_PYTHON_MODULE
內的代碼開始看。
class_<A>
的定義看上去和前面的例子沒有太大差別迈喉,只是多導出了一個成員.def_readwrite("vB", &A::m_vB)
绍刮。即使這個成員變量是vector<B>
類型的,在這里也不需要特殊對待挨摸;class_<B>
就是導出一個類孩革,包含兩個共有數據成員。這里也沒什么特別的得运;class_<std::vector<A> >("VecA")
和class_<std::vector<B> >("VecB")
是本例的重點膝蜈,導出了兩個不同的vector
類型,因為在c++里熔掺,vector
是一個類模板饱搏,vector<A>
和vector<B>
才是兩個具體的類型;printA
置逻,addA
推沸,accumulate
是三個導出的函數。即使其參數是vector<A>
類型,也無需特別對待鬓催;
除此之外肺素,注意到為類型A和類型B定義了==
操作符,這是boost.python在導出某種類型的vector時需要的深浮,在內部某個地方用到了==
操作符压怠。如果僅導出類型,不導出類型的向量飞苇,是不需要==
操作符的,如前面的例子所示蜗顽。
編譯鏈接后布卡,通過python命令行測試一下:
>>>import test_class
>>>a1 = test_class.A()
>>>b1 = test_class.B() # 實例化一個B
>>>b1.pos = 1
>>>b1.len = 1
>>>b2 = test_class.B() # 實例化另一個B
>>>b2.pos = 2
>>>b2.len = 2
>>>a1.vB.append(b1) # a1.vB是vector<B>在python中對應類型的對象,接口類似list雇盖,但只能添加B類型的對象
>>>a1.vB.append(b2)
>>>print a1.vB[-1].len # a1.vB支持list的下標引用
2
>>>a1.set(1)
>>>a2 = test_class.A(2)
>>>a3 = test_class.A(3)
>>>vA = test_class.VecA() # vector<A>在python中對應的類型
>>>vA.append(a1)
>>>vA.append(a2)
>>>vA.append(a3)
>>>print accumulate(vA) # 調用以vector<A>為參數的函數
6
對于導出的python模塊來說忿等,一切在python中會被引用到的變量,其所屬類型(基本數據類型除外)都需要被明確導出崔挖,也就是都需要在BOOST_PYTHON_MODULE
里被定義贸街。如本例中的vector<B>
,盡管沒有函數以該類型為參數狸相,但如果想要在python代碼里引用A
的成員vB
薛匪,就需要導出它,否則會拋異常脓鹃。相反逸尖,如果不需要在python代碼里引用這個成員,則不需要導出vector<B>
這個類型瘸右,而且在class_<A>
的定義中也應把.def_readwrite("vB", &A::m_vB)
去掉娇跟。如果不導出vector<B>
類型,但在class_<A>
的定義中通過.def_readwrite("vB", &A::m_vB)
導出了該成員太颤,編譯不會出問題苞俘,使用python模塊也不會出問題,但只要代碼引用到A.vB
就會拋異常龄章,相當于埋了一個坑吃谣。
從實用的角度看,這樣一個流程可能會比較有效瓦堵。首先基协,確定需要導出的函數及類型。然后檢查函數(包括成員函數)參數及返回值的類型菇用,非基本類型需要被導出澜驮;檢查導出的類成員變量,如果不是基本類型惋鸥,其類型也要導出杂穷。如此直到沒有新的類型需要被添加為止悍缠。
總結
Boost.python的文檔感覺比較少,很多問題和trick都是在stack overflow上看到然后再試驗的耐量。據了解飞蚓,Boost.python支持更為豐富的c++特性,這里只用到了一小部分廊蜒。