boost.python筆記

boost.python筆記

標(biāo)簽:boost.python


簡(jiǎn)介

Boost.python是什么?
它是boost庫(kù)的一部分,隨boost一起安裝,用來實(shí)現(xiàn)C++和Python代碼的交互。

使用Boost.python有什么特點(diǎn)?
不需要修改原有的C++代碼,支持比較豐富的C++特性。不會(huì)生成額外的python代碼(像SWIG那樣),但是需要寫一部分C++的封裝代碼。

我只用到了其功能的一部分,把C/C++實(shí)現(xiàn)的功能封裝為可供python直接調(diào)用的.so庫(kù)。具體場(chǎng)景是,有一個(gè)C++模塊通過thrift封裝為RPC,python代碼通過PRC調(diào)用請(qǐng)求服務(wù)。由于調(diào)用頻次較多,RPC調(diào)用開銷成為一個(gè)很耗時(shí)的部分,因此想直接通過python對(duì)原模塊功能進(jìn)行調(diào)用。

以前了解SWIG可以實(shí)現(xiàn)這個(gè)需求。本已開始看SWIG的文檔,但突然又想看一下還有沒有別的方法,于是在stack overflow上搜了一些問題,發(fā)現(xiàn)不少人推薦Boost.python,于是打算拿它來試一試。

如何使用Boost.python

首先,前提是安裝了開發(fā)環(huán)境。

  1. 安裝了boost開發(fā)環(huán)境。安裝了頭文件和動(dòng)態(tài)庫(kù)。
  2. 安裝了python開發(fā)環(huán)境。安裝了頭文件和動(dòng)態(tài)庫(kù)。

然后,就是寫代碼了,這是個(gè)沒辦法避免的事情!??!

需要自己動(dòng)手的有兩個(gè)地方。一個(gè)是xxxxxx_wrapper.cpp文件,文件名無所謂,其核心目的是定義導(dǎo)出的python模塊的名稱,以及需要導(dǎo)出的類、函數(shù)等。另一個(gè)是需要修改一下你的Makefile,來編譯、鏈接這個(gè)so庫(kù)。

先來看一下xxxxxx_wrapper.cpp。一般情況下,它的內(nèi)容跟下面的代碼比較接近。

#include <boost/python.hpp>
// 其他需要包含的頭文件,與具體業(yè)務(wù)有關(guān)

namespace py = boost::python;

// 其他函數(shù),可能包括一些用于類型轉(zhuǎn)換和封裝的

BOOST_PYTHON_MODULE(my_module_name)
{
    // 導(dǎo)出普通函數(shù)
    def("fun_name_in_python", &fun_name_in_c);
    
    // 導(dǎo)出類及部分成員
    class_<ClassNameInCpp>("ClassNameInPython", init<std::string>())               //類名,默認(rèn)構(gòu)造函數(shù)
        .def(init<double>())                                                       //其他構(gòu)造函數(shù)
        .def("memberFunNameInPython", &ClassNameInCpp::memberFunNameInCpp)         //成員函數(shù)
        .def_readwrite("dataMemberInPython", &ClassNameInCpp::dataMemberInCpp)     //普通成員變量
        .def_readonly("dataMemberInPython_2", &ClassNameInCpp::dataMemberInCpp_2)  //只讀成員變量
    ;
}

這個(gè)文件的核心目的體現(xiàn)在BOOST_PYTHON_MODULE里,定義需要導(dǎo)出給python的東西。

在Makefile里,需要增加一條用來編譯導(dǎo)出的.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}

編譯及鏈接參數(shù)的作用如下,其他參數(shù)由具體項(xiàng)目的業(yè)務(wù)邏輯決定:

  1. -o my_module_name.so,這里的模塊名需要和xxxxxx_wrapper.cpp文件里BOOST_PYTHON_MODULE(my_module_name)一致;
  2. -I${BOOST_INCLUDE_PATH} -I${PYTHON_INCLUDE_PATH}是編譯需要的;
  3. -L${BOOST_LIB_DIR} -lboost_python -shared -fPIC是鏈接需要的;
  4. ${MY_SRC_FILES}包含了xxxxxxx_wrapper.cpp以及業(yè)務(wù)邏輯需要的其他.cpp,.c文件;

至此,我們便有了my_module_name.so這個(gè)可以被python調(diào)用的模塊了。測(cè)試一下吧。

>>>import my_moduel_name
>>>help(my_module_name)

可以看到被導(dǎo)出的類及函數(shù),然后可以按照python的習(xí)慣來使用這些類和函數(shù)了。

如何寫wrapper

以一個(gè)實(shí)例為框架來解釋吧,內(nèi)容包括普通函數(shù)、類、數(shù)據(jù)成員、成員函數(shù)、通過參數(shù)傳遞結(jié)果、容器。其他特性沒有用到,也沒有測(cè)試。

先來看一下業(yè)務(wù)邏輯的代碼。包含一個(gè)類,一個(gè)以類對(duì)象為參數(shù)的函數(shù),一個(gè)通過引用修改類對(duì)象的函數(shù)。

//test_class.h

// 定義一個(gè)類
class A
{
    public:
        A(){privateVal=0;}                      //默認(rèn)構(gòu)造函數(shù)
        A(int val){privateVal=val;}             //帶參數(shù)的構(gòu)造函數(shù)
        void set(int val){privateVal=val;}      //成員函數(shù)
        int get() const {return privateVal;};   //成員函數(shù)
        int publicVal;                          //公共數(shù)據(jù)成員
    private:
        int privateVal;                         //私有數(shù)據(jù)成員
};

int addA(A &a, int addVal);                   //普通函數(shù),有返回值,通過引用修改參數(shù)
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文件,這里實(shí)際名為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;
    // 導(dǎo)出類
    class_<A>("A", init<>())                            //如果默認(rèn)構(gòu)造函數(shù)沒有參數(shù),可以省略
        .def(init<int>())                               //其他構(gòu)造函數(shù)
        .def("get", &A::get)                            //成員函數(shù)
        .def("set", &A::set)                            //成員函數(shù)
        .def_readwrite("publicVal", &A::publicVal)      //數(shù)據(jù)成員,當(dāng)然是公共的
    ;   
    def("printA", &printA);
    def("addA", &addA);
}

通過python命令行測(cè)試一下

>>>import test_class
>>>a = test_class.A(5)
>>>ret = addA(a, 10)
>>>print ret
15
>>>print a.get()
15

到目前為止,整個(gè)過程都很順利。需要額外寫的代碼很少,也很規(guī)整,與某種IDL的寫法接近,只需要“聲明”一下,剩下的事情都交給編譯器及庫(kù)完成。但有的時(shí)候,這個(gè)過程就不這么順利了,我們需要額外寫一些轉(zhuǎn)換及封裝。比如在這一部分最開始提到的容器,上面的代碼就沒有涉及。

下面的代碼,我們對(duì)上面的例子做了一些擴(kuò)展。第一,對(duì)類A增加了一個(gè)vector成員,需要在python代碼里引用該成員;第二,增加了一個(gè)函數(shù),以vector為參數(shù),需要在python代碼里直接調(diào)用該函數(shù)。下面我們就來解釋與容器有關(guān)的導(dǎo)出。

#include<vector>

class B;

class A
{
    public:
        A(){privateVal=0;}                      //默認(rèn)構(gòu)造函數(shù)
        A(int val){privateVal=val;}             //帶參數(shù)的構(gòu)造函數(shù)
        void set(int val){privateVal=val;}      //成員函數(shù)
        int get() const {return privateVal;};   //成員函數(shù)
        int publicVal;                          //公共數(shù)據(jù)成員
        std::vector<B> m_vB;
    private:
        int privateVal;                           //私有數(shù)據(jù)成員
};

class B
{
    public:
        B(){}
        ~B(){}
        int pos;
        int len;
};

int accumulate(const std::vector<A>& v_A);
int addA(A &a, int addVal);                   //普通函數(shù),有返回值,通過引用修改參數(shù)
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;
}

首先,需要明白一點(diǎn),c++中的vector不等于python中的list,雖然它們看上去比較相似。Boost.python中有與python的list對(duì)應(yīng)的東西,是boost::python::list,如果在python代碼里以list為參數(shù)調(diào)用某個(gè)方法,則在c++代碼中這個(gè)參數(shù)被自動(dòng)映射為boost::python::list,不是vector。既然這樣,如果我們不打算修改原有的C++代碼,又想調(diào)用以vector為參數(shù)的函數(shù),該怎么辦呢?

目前我了解的方法由兩種:

  1. 在C++代碼里對(duì)以vector為參數(shù)的函數(shù)進(jìn)行一層封裝,封裝為以boost::python::list為參數(shù)的函數(shù),導(dǎo)出封裝后的函數(shù)。在函數(shù)里通過boost::python::extract_<T>對(duì)list里的所有成員進(jìn)行提取,將其由boost::python::object對(duì)象變?yōu)門類型的對(duì)象,然后存于vector<T>中,再調(diào)用以vector<T>為參數(shù)的函數(shù)。
    如果需要返回list或者原函數(shù)對(duì)vector參數(shù)的內(nèi)容作了修改,需要再將調(diào)用函數(shù)后的vector內(nèi)的每個(gè)元素放回list里。
  2. 直接導(dǎo)出vector<T>類型。此時(shí)vector<T>本身作為一個(gè)類型被導(dǎo)出給python代碼,與普通的類具有同等地位。但是,與普通類不同的是,它通過模板vector_indexing_suite<std::vector<T> >()導(dǎo)出,自動(dòng)實(shí)現(xiàn)了append,slice,__len__等方法,在python里可以像使用list那樣操作這個(gè)被導(dǎo)出的vector類。而且,以vector<T>為參數(shù)的函數(shù),在通過def導(dǎo)出給python時(shí),其參數(shù)會(huì)被自動(dòng)映射為vector_indexing_suite<std::vector<T> >。同理,python代碼傳入的通過vector_indexing_suite導(dǎo)出的容器對(duì)象,也會(huì)在c++代碼里被自動(dòng)轉(zhuǎn)換為vector,這里無需顯式地寫轉(zhuǎn)換函數(shù)。

下面的代碼是wrapper文件,使用第二種方法,即直接導(dǎo)出vector類型。

為什么這么做呢?因?yàn)锳里有個(gè)vector成員,要導(dǎo)出這個(gè)成員,必須導(dǎo)出這個(gè)vector<B>這個(gè)類型,否則還需要對(duì)類A再做一層封裝,讓它包含一個(gè)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<>())                            //如果默認(rèn)構(gòu)造函數(shù)沒有參數(shù),可以省略
        .def(init<int>())                               //其他構(gòu)造函數(shù)
        .def("get", &A::get)                            //成員函數(shù)
        .def("set", &A::set)                            //成員函數(shù)
        .def_readwrite("publicVal", &A::publicVal)      //數(shù)據(jù)成員,當(dāng)然是公共的
        .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內(nèi)的代碼開始看。

  • class_<A>的定義看上去和前面的例子沒有太大差別,只是多導(dǎo)出了一個(gè)成員.def_readwrite("vB", &A::m_vB)。即使這個(gè)成員變量是vector<B>類型的,在這里也不需要特殊對(duì)待;
  • class_<B>就是導(dǎo)出一個(gè)類,包含兩個(gè)共有數(shù)據(jù)成員。這里也沒什么特別的;
  • class_<std::vector<A> >("VecA")class_<std::vector<B> >("VecB")是本例的重點(diǎn),導(dǎo)出了兩個(gè)不同的vector類型,因?yàn)樵赾++里,vector是一個(gè)類模板,vector<A>vector<B>才是兩個(gè)具體的類型;
  • printA,addA,accumulate是三個(gè)導(dǎo)出的函數(shù)。即使其參數(shù)是vector<A>類型,也無需特別對(duì)待;

除此之外,注意到為類型A和類型B定義了==操作符,這是boost.python在導(dǎo)出某種類型的vector時(shí)需要的,在內(nèi)部某個(gè)地方用到了==操作符。如果僅導(dǎo)出類型,不導(dǎo)出類型的向量,是不需要==操作符的,如前面的例子所示。

編譯鏈接后,通過python命令行測(cè)試一下:

>>>import test_class
>>>a1 = test_class.A()
>>>b1 = test_class.B()    # 實(shí)例化一個(gè)B
>>>b1.pos = 1
>>>b1.len = 1
>>>b2 = test_class.B()    # 實(shí)例化另一個(gè)B
>>>b2.pos = 2
>>>b2.len = 2
>>>a1.vB.append(b1)      # a1.vB是vector<B>在python中對(duì)應(yīng)類型的對(duì)象,接口類似list,但只能添加B類型的對(duì)象
>>>a1.vB.append(b2)
>>>print a1.vB[-1].len        # a1.vB支持list的下標(biāo)引用
2
>>>a1.set(1)
>>>a2 = test_class.A(2)
>>>a3 = test_class.A(3)
>>>vA = test_class.VecA()    # vector<A>在python中對(duì)應(yīng)的類型
>>>vA.append(a1)
>>>vA.append(a2)
>>>vA.append(a3)
>>>print accumulate(vA)      # 調(diào)用以vector<A>為參數(shù)的函數(shù)
6

對(duì)于導(dǎo)出的python模塊來說,一切在python中會(huì)被引用到的變量,其所屬類型(基本數(shù)據(jù)類型除外)都需要被明確導(dǎo)出,也就是都需要在BOOST_PYTHON_MODULE里被定義。如本例中的vector<B>,盡管沒有函數(shù)以該類型為參數(shù),但如果想要在python代碼里引用A的成員vB,就需要導(dǎo)出它,否則會(huì)拋異常。相反,如果不需要在python代碼里引用這個(gè)成員,則不需要導(dǎo)出vector<B>這個(gè)類型,而且在class_<A>的定義中也應(yīng)把.def_readwrite("vB", &A::m_vB)去掉。如果不導(dǎo)出vector<B>類型,但在class_<A>的定義中通過.def_readwrite("vB", &A::m_vB)導(dǎo)出了該成員,編譯不會(huì)出問題,使用python模塊也不會(huì)出問題,但只要代碼引用到A.vB就會(huì)拋異常,相當(dāng)于埋了一個(gè)坑。

從實(shí)用的角度看,這樣一個(gè)流程可能會(huì)比較有效。首先,確定需要導(dǎo)出的函數(shù)及類型。然后檢查函數(shù)(包括成員函數(shù))參數(shù)及返回值的類型,非基本類型需要被導(dǎo)出;檢查導(dǎo)出的類成員變量,如果不是基本類型,其類型也要導(dǎo)出。如此直到?jīng)]有新的類型需要被添加為止。

總結(jié)

Boost.python的文檔感覺比較少,很多問題和trick都是在stack overflow上看到然后再試驗(yàn)的。據(jù)了解,Boost.python支持更為豐富的c++特性,這里只用到了一小部分。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容