C++類和結(jié)構(gòu)以類似的簡潔的接口來暴露。假設(shè)有:
struct World
{
void set(std::string msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
};
下面的代碼將在我們的擴(kuò)展模塊里暴露它:
#include <boost/python.hpp>
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World")
.def("greet",&World::greet)
.def("set",&World::set)
;
}
盡管上述代碼有某種熟悉的Pythonic的感覺,但語法有時還是有點(diǎn)令人迷惑,因?yàn)樗雌饋聿幌袢藗兞?xí)慣的C++代碼。但是,這仍然只是標(biāo)準(zhǔn)C++。因?yàn)樗鼈冹`活的語法和操作符重載,C++和Python都很適于定義特定領(lǐng)域(子)語言( domain-specific (sub)languages) (DSLs)。那就是我們在Boost.Python里所做的。把代碼拆開來看:
class_<World>("World")
構(gòu)造類型class_<World>的未命名對象,把"World"傳給它的構(gòu)造器。這將在擴(kuò)展模塊里創(chuàng)建一個叫World的new-style Python類,并把它和C++類型World在Boost.Python的類型轉(zhuǎn)換注冊表里關(guān)聯(lián)起來。我們也可以這么寫:
class_<World> w("World");
但那樣做的話會更繁瑣,因?yàn)槲覀儾坏貌辉俅蚊鹷以調(diào)用它的def()成員函數(shù):
w.def("greet",&World::greet)
原來的例子里表示成員進(jìn)入的點(diǎn)的位置沒有什么特別的:C++允許任意的空白符出現(xiàn)在表意符號(token)的任一邊,把點(diǎn)放在每行的開始允許用統(tǒng)一的語法把連續(xù)的調(diào)用都串起來,不管我們想串多少都行。另一個允許的串接的事實(shí)是class_<>成員函數(shù)都返回對*this的引用。
因此原來的例子等同于:
class_<World> w("World");
w.def("greet",&World::greet);
w.def("set",&World::set);
能這樣拆分Boost.Python類包裝層的組成部分有時候是有用的,但本文的剩下部分將一直使用簡潔的語法。
最后來看包裝類被使用的情況:
>>> import hello
>>> planet = hello.World()
>>> planet.set('howdy')
>>> planet.greet()
'howdy'
因?yàn)槲覀兊腤orld類只是一個簡單的struct,它有一個隱式的無參數(shù)(no-argument)(nullary)構(gòu)造器。Boost.Python默認(rèn)暴露nullary構(gòu)造器,這就是我們可以像下面這樣寫的原因:
>>> planet = hello.World()
然而不管哪門語言,設(shè)計(jì)得好的類可能都需要構(gòu)造器參數(shù),以建立他們的不變量(invariants)。在Python里,__init__只是一個特殊名稱的成員函數(shù)(method),與這不同,C++里的構(gòu)造器不能像普通成員函數(shù)那樣處理。特別是我們不能取它的地址: &World::World這樣會被報錯。庫提供了一個不同的接口來指定構(gòu)造器。假設(shè)有:
struct World
{
World(std::string msg); // 添加的構(gòu)造器
...
我們可以這樣修改包裝代碼:
class_<World>("World",init<std::string>())
.def(init<double,double>())
...
當(dāng)然,C++類可能還有其他的構(gòu)造器,我們也可以暴露他們,只需要向def()傳遞更多init<…>的實(shí)例:
class <World>("World",init<std::string>())
.def(init<double,double>())
...
Boost.Python允許被包裝的函數(shù),成員函數(shù)以及構(gòu)造器被重載,以映射C++重載。
C++中的任何可公共的訪問的數(shù)據(jù)成員都能輕易的被包裝成只讀或者只寫屬性(attributes):
class_<World>("World",init<std::string>())
.def_readonly("msg",&World::msg)
...
并直接在Python里使用:
>>> planet = hello.World('howdy')
>>> planet.msg
'howdy'
這不會導(dǎo)致添加屬性到World實(shí)例__dict__,從而在包裝大型數(shù)據(jù)結(jié)構(gòu)時節(jié)省大量的內(nèi)存。實(shí)際上,除非從Python顯式添加屬性,否則實(shí)例__dict__根本不會被創(chuàng)建。Python的這種能力來源于新的Python 2.2 類型系統(tǒng),尤其是descriptor接口和property類型。
在C++里,可公共的訪問的數(shù)據(jù)成員被認(rèn)為是糟糕設(shè)計(jì)的表現(xiàn),因?yàn)樗麄兤茐牧朔庋b(encapsulation),文體向?qū)?style guides)通常指示代之以"getter" 和 "setter"函數(shù)。然而在Python里,__getattr__,__setattr__和從2.2開始有的property意味著屬性進(jìn)入只是程序員控制下的封裝得更好的語法工具。通過讓Python property對用戶直接可用,Boost.Python連接了二者的不同慣用語。如果msg是私有的,我們?nèi)匀荒馨阉┞稙镻ython里的屬性:
class_<World>("World",init<std::string>())
.add_property("msg",&World::greet,&World::set)
...
上面的例子映射了人們熟悉的Python 2.2+里的property用法:
>>> class World(object):
... __init__(self,msg):
... self.__msg = msg
... def greet(self):
... return self.__msg
... def set(self,msg):
... self.__msg = msg
... msg = property(greet,set)
能給用戶定義類型定義算術(shù)操作符一直是兩門語言的數(shù)值計(jì)算取得成功一個重要因素。像 Numpy 這樣的軟件包的成功證明了在擴(kuò)展模塊中暴露操作符能產(chǎn)生巨大能量。Boost.Python給包裝操作符重載提供了簡潔的機(jī)制。下面是包裝Boost的有理數(shù)庫( rational number library)的代碼的片斷:
class_<rational<int> >("rational_int")
.def(init<int,int>()) // constructor,e.g. rational_int(3,4)
.def("numerator",&rational<int>::numerator)
.def("denominator",&rational<int>::denominator)
.def(-self) // __neg__ (unary minus)
.def(self + self) // __add__ (homogeneous)
.def(self * self) // __mul__
.def(self + int()) // __add__ (heterogenous)
.def(int() + self) // __radd__
這種魔法是通過簡單的應(yīng)用"表達(dá)式模板"("expression templates") [VELD1995] 來施加的,"表達(dá)式模板"是一種最初為優(yōu)化高性能矩陣代數(shù)表達(dá)式而開發(fā)的技術(shù)。本質(zhì)是不立即進(jìn)行計(jì)算,而重載操作符來構(gòu)造描述計(jì)算的類型。在矩陣代數(shù)里,當(dāng)考慮整個表達(dá)式的結(jié)構(gòu),而不是"貪婪的"對每步操作求值時,經(jīng)常可以獲得戲劇性的優(yōu)化。Boost.Python用同樣的技術(shù)來構(gòu)建基于包含 self 的表達(dá)式的適當(dāng)?shù)腜ython 成員函數(shù)對象(method object)。
要在Boost.Python里描述C++繼承關(guān)系,可以像下面這樣把可選的bases<…>參數(shù)添加到class_<…>模板參數(shù)表里:
class_<Derived,bases<Base1,Base2> >("Derived")
...
這樣有兩個效果:
- 當(dāng)class_<…>被創(chuàng)建時,在Boost.Python的注冊表里查找對應(yīng)于Base1和Base2的Python類型對象,然后作為新的Python衍生類 型對象的基類。因而暴露給Base1和Base2的成員函數(shù)自動成為了衍生類型的成員。因?yàn)樽员硎侨值模词贡┞堆苌愋偷哪?塊和它的任一基類的模塊不同,繼承同樣有效。
- 衍生類到基類的C++轉(zhuǎn)換被添加到Boost.Python注冊表。因此期待任一基類對象(的指針或引用)的被包裝C++成員函數(shù)可以被包裝 了任一基類的衍生實(shí)例的對象調(diào)用。類T的被包裝成員函數(shù)被視為具有隱式的第一個參數(shù)T&,那么為了讓衍生對象能調(diào)用基類成 員函數(shù),這些轉(zhuǎn)換是必需的。
當(dāng)然從被包裝的C++類實(shí)例衍生新的Python類是可能的。這是因?yàn)锽oost.Python使用了new-style class系統(tǒng),這套系統(tǒng)在Python內(nèi)置類型上工作良好。但有一個重要細(xì)節(jié)不同: Python內(nèi)置類型一般在__new__函數(shù)里建立不變量(invariants),從而衍生類不用在調(diào)用它的成員函數(shù)前調(diào)用基類的__init__:
>>> class L(list):
... def __init__(self):
... pass
...
>>> L().reverse()
>>>
因?yàn)镃++對象構(gòu)造是一步操作(one-step operation),直到參數(shù)可用C++實(shí)例數(shù)據(jù)才能被構(gòu)造,在__init__函數(shù)里:
>>> class D(SomeBoostPythonClass):
... def __init__(self):
... pass
...
>>> D().some_boost_python_method()
Traceback (most recent call last):
File "<stdin>",line 1,in ?
TypeError: bad argument type for built-in operation
發(fā)生錯誤的原因是Boost.Python 在實(shí)例D里找不到類型SomeBoostPythonClass的實(shí)例數(shù)據(jù);D的__init__函數(shù)遮蓋了基類的構(gòu)造函數(shù)。可以通過刪除D的__init__函數(shù)或是讓它顯式的調(diào)用SomeBoostPythonClass.__init__(…)來糾正錯誤。
在Python里從擴(kuò)展類衍生新的類型沒太大意思,除非它們在C++里能被多態(tài)的使用。換句話說,當(dāng)在C++里通過基類指針/引用調(diào)用Python成員函數(shù)時,Python成員函數(shù)的實(shí)現(xiàn)應(yīng)該看起來像是覆蓋(override)了C++虛函數(shù)的實(shí)現(xiàn)。因?yàn)橐淖兲摵瘮?shù)的行為的唯一方法是在衍生類里覆蓋(override)它,用戶必須構(gòu)造一個特殊的衍生類來分派(dispatch)多態(tài)類的虛函數(shù)。:
//
// 要包裝的接口:
//
class Base
{
public:
virtual int f(std::string x) { return 42; }
virtual ~Base();
};
int calls_f(Base const& b,std::string x) { return b.f(x); }
//
// 包裝代碼
//
// 分派者類(Dispatcher class)
struct BaseWrap : Base
{
// 儲存指向Python對象的指針
BaseWrap(PyObject* self_) : self(self_) {}
PyObject* self;
// 當(dāng)f沒有被覆蓋(override)時的缺省實(shí)現(xiàn)
int f_default(std::string x) { return this->Base::f(x); }
// 分派實(shí)現(xiàn)
int f(std::string x) { return call_method<int>(self,"f",x); }
};
...
def("calls_f",calls_f);
class_<Base,BaseWrap>("Base")
.def("f",&Base::f,&BaseWrap::f_default)
;
下面是一些python演示代碼:
>>> class Derived(Base):
... def f(self,s):
... return len(s)
...
>>> calls_f(Base(),'foo')
42
>>> calls_f(Derived(),'forty-two')
9
對分派者類(Dispatcher class),要注意:
- 允許用Python進(jìn)行覆蓋(override)的關(guān)鍵因素是call_method調(diào)用,它使用了和用來包裝C++函數(shù)的注冊表相同的全局類型轉(zhuǎn)換注冊 表(global type conversion registry),來把參數(shù)從C++轉(zhuǎn)換成Python,把返回類型從Python轉(zhuǎn)換成C++
- 任何你希望包裝的構(gòu)造器署名(signatures)必須有一個的相同的初始PyObject*參數(shù)。
- 分派者必須保存這個參數(shù)以便調(diào)用call_method時使用。
- 當(dāng)被暴露的函數(shù)不是純虛函數(shù)時,需要f_default成員函數(shù)。在類型BaseWrap的對象里,沒有其他方式可以調(diào)用Base::f,因?yàn)樗?覆蓋(override)了。
無可否認(rèn),重復(fù)這種公式化的流程是冗長乏味的。尤其是項(xiàng)目里有大量多態(tài)類的時候。這反映了C++編譯時內(nèi)省能力的必然限制:無法枚舉類的成員來判斷哪個是虛函數(shù)。不過,一個很有希望的項(xiàng)目已經(jīng)啟動,致力于寫一個前端程序來從C++頭文件自動生成這些分派者類(以及其它包裝代碼)。
Pyste 是由Bruno da Silva de Oliveira開發(fā)的,基于 GCC_XML 構(gòu)建。GCC_XML可以生成XML版本的GCC內(nèi)部程序描述。GCC是一種高度符合(譯注:C++標(biāo)準(zhǔn))的編譯器,從而確保了對最復(fù)雜的模板代碼的正確處理和對底層類型系統(tǒng)的完全訪問。和Boost.Python的哲學(xué)一致,Pyste接口描述既不侵入被包裝的代碼,也不用某種不熟悉的語言來表達(dá),相反:它是100%的純Python腳本。如果Pyste成功的話,將標(biāo)志著我們的很多用戶不用再什么都直接用C++包裝。它將允許我們選擇把一些元程序(metaprogram)代碼從C++移動到Python。我們期待不久后不僅用戶,Boost.Python開發(fā)者自己也能以混合的思路來考慮("thinking hybrid")他們自己的代碼。
序列化是把內(nèi)存中的對象轉(zhuǎn)換成可以保存到磁盤上或通過網(wǎng)絡(luò)傳送的格式的過程。序列化后的對象(最常見的是簡單字符串)可以被重新取得并轉(zhuǎn)換回原來的對象。好的序列化系統(tǒng)能夠自動轉(zhuǎn)換整個對象層次(object hierarchies)。Python的標(biāo)準(zhǔn)pickle模塊正是這樣的系統(tǒng)。它利用語言強(qiáng)大的運(yùn)行時內(nèi)省來序列化幾乎是任意的用戶定義對象。加上一些簡單的非侵入限定,這種強(qiáng)大的設(shè)施可以被擴(kuò)展成對被包裝的C++對象也有效。下面是一個例子:
#include <string>
struct World
{
World(std::string a_msg) : msg(a_msg) {}
std::string greet() const { return msg; }
std::string msg;
};
#include <boost/python.hpp>
using namespace boost::python;
struct World_picklers : pickle_suite
{
static tuple
getinitargs(World const& w) { return make_tuple(w.greet()); }
};
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World",init<std::string>())
.def("greet",&World::greet)
.def_pickle(World_picklers())
;
}
現(xiàn)在讓我們創(chuàng)建一個World對象并把它放到磁盤上:
>>> import hello
>>> import pickle
>>> a_world = hello.World("howdy")
>>> pickle.dump(a_world,open("my_world","w"))
在可能不同的計(jì)算機(jī)的可能不同的操作系統(tǒng)的可能不同的腳本中:
>>> import pickle
>>> resurrected_world = pickle.load(open("my_world","r"))
>>> resurrected_world.greet()
'howdy'
當(dāng)然也可以用cPickle來獲得更快的處理速度。
Boost.Python的pickle_suite完全支持標(biāo)準(zhǔn)Python文檔定義的pickle協(xié)議。類似Python里的__getinitargs__函數(shù),pickle_suite的getinitargs()負(fù)責(zé)創(chuàng)建參數(shù)tuple來重建被pickle了的對象。Python pickle協(xié)議中的其他元素,__getstate__ 和__setstate__可以通過C++ getstate和setstate函數(shù)來可選的提供。C++的靜態(tài)類型系統(tǒng)允許庫在編譯時確保沒有意義的函數(shù)組合(例如:沒有setstate 就getstate)不會被使用。
要想序列化更復(fù)雜的C++對象需要做比上面的例子稍微多點(diǎn)的工作。幸運(yùn)的是Object接口(參見下一節(jié))在保持代碼便于管理上幫了大忙。
無所不在的PyObject*,手動引用計(jì)數(shù),需要記住是哪個API調(diào)用返回了"新的"(自身擁有的)引用或是"借來的"(原始的)引用,這些可能有經(jīng)驗(yàn)的C語言擴(kuò)展模塊的作者都熟悉。這些約束不僅麻煩,更是錯誤的主要來源,尤其是存在異常的時候。
Boost.Python提供了一個object類,它自動化了引用計(jì)數(shù)并提供任意類型的C++對象到Python的轉(zhuǎn)換。這極大的減輕了未來的擴(kuò)展模塊作者的學(xué)習(xí)負(fù)擔(dān)。
從任一類型創(chuàng)建object極度簡單:
object s("hello,world"); // s 管理一個Python字符串
object可以和所有其它類型進(jìn)行模板化的交互,自動的進(jìn)行到Python的轉(zhuǎn)換。這一切自然得很容易被忽略不計(jì):
object ten_Os = 10 * s[4]; // -> "oooooooooo"
上面的例子中,在索引和乘法操作被調(diào)用前,4和10被轉(zhuǎn)換成了Python對象。
extract<T>類模板可以用來把Python對對象轉(zhuǎn)換成C++類型:
double x = extract<double>(o);
如果任一方向的轉(zhuǎn)換不能執(zhí)行,將在運(yùn)行時拋出一個適當(dāng)?shù)漠惓!?/p>
伴隨object類型的是一套衍生類型,盡可能的映射Python的內(nèi)置類型(list,dict,tuple等)。這樣就能方便的從C++操作這些高級類型了:
dict d;
d["some"] = "thing";
d["lucky_number"] = 13;
list l = d.keys();
這看起來和工作起來幾乎就像是通常的python代碼,但它實(shí)際上是純的C++。當(dāng)然我們能包裝接受或返回object實(shí)例的函數(shù)。