Author: David Abrahams

摘要

Boost.Python是一個開源的C++程序庫,提供類似IDL的接口來把C++類和函數(shù)綁定到Python。借助于C++強(qiáng)大的編譯時內(nèi)省(introspection)能力和最新的元編程(metaprogramming)技術(shù),它完全用C++來實(shí)現(xiàn),而不用引入新的語法。Boost.Python豐富的特性和高級接口使從底層起按混合系統(tǒng)的方式設(shè)計(jì)組件成為可能,從而使程序員可以輕松和連貫的同時使用C++高效的編譯時多態(tài)和Python極其方便的運(yùn)行時多態(tài)。

簡介

作為兩門語言,python和C++在很多方面不一樣。C++被編譯為機(jī)器碼,python被解釋(interpreted)執(zhí)行。Python的動態(tài)類型(type)系統(tǒng)經(jīng)常被認(rèn)為是靈活性的基礎(chǔ),C++的靜態(tài)類型是效率的基石。C++有復(fù)雜艱深的編譯時元語言(meta-language),而在python里,實(shí)際上一切都在運(yùn)行時發(fā)生。然而對很多程序員來說,這些不同恰好意味著Python和C++是彼此的完美補(bǔ)足。Python 程序里的性能瓶頸部分可以用C++來重寫,從而最大化速度。強(qiáng)大的C++程序庫的作者選擇Python作為中間件(middleware)語言,從而獲得靈活的系統(tǒng)集成能力。此外,表面的不同掩蓋了二者非常類似的一些地方:

  • ‘C’-家族的控制結(jié)構(gòu) (if,while,for…)
  • 支持面向?qū)ο?object-orientation),函數(shù)式編程(functional programming),泛型編程(generic programming)(它們都是多范 式語言(multi-paradigm languages))
  • 認(rèn)同語法可變性(syntactic variability)在提高代碼的可讀性和表達(dá)力上的重要作用,提供了對操作符重載的廣泛支持
  • 高級概念,如集合(collections),迭代器(iterators)等
  • 高級封裝機(jī)制(C++: namespaces,Python: modules)支持可重用程序庫的設(shè)計(jì)
  • 異常處理機(jī)制提供了有效的錯誤管理
  • 被普遍使用的C++習(xí)慣用語,如handle/body classes和引用被計(jì)數(shù)的智能指針(reference-counted smart pointers)對應(yīng)Python 的引用語義(reference semantics)

考慮到Python豐富的’C'協(xié)作API,原則上把C++的類型和函數(shù)以類似于暴露給C++的接口暴露給Python是可能的。然而,單是Python提供的這種設(shè)施對集成C++的支持比較弱。和C++,Python相比,’C'的抽象機(jī)制非常初級,而且完全不支持異常處理。’C'擴(kuò)展模塊的作者必須手動管理引用計(jì)數(shù),這不但讓人惱火的麻煩和單調(diào),還極度容易出錯。傳統(tǒng)的擴(kuò)展模塊容易產(chǎn)生重復(fù)的樣板代碼(boilerplate code),從而難于維護(hù),尤其是要包裝的API很復(fù)雜時。

上述限制導(dǎo)致了一些包裝系統(tǒng)的開發(fā)。SWIG_ 可能是集成C/C++和Python的包裝系統(tǒng)中最流行的。一個更近的例子是 SIP ,它專門設(shè)計(jì)來提供 Qt 圖形用戶界面庫的Python接口。SWIG和SIP都引入了它們專有的語言來實(shí)現(xiàn)語言間綁定。這當(dāng)然有它的好處,但不得不應(yīng)付三種不同的語言(Python,C/C++和接口語言)也帶來了實(shí)際的和心理上的困難。 CXX 軟件包展示出它是一個有趣的包裝系統(tǒng)。它說明了至少一部份Python ‘C’ API可以通過用戶友好得多的C++接口來包裝和表現(xiàn)。然而,和SWIG和SIP不一樣,CXX不支持把C++類包裝成新的Python類型。

Boost.Python 的特性和目標(biāo)和很多這樣的系統(tǒng)有相當(dāng)程度的重疊。就是說,Boost.Python試圖最大化便利性和靈活性,而不引入單獨(dú)的包裝語言。相反,它在幕后用靜態(tài)元編程技術(shù)管理很多復(fù)雜問題,賦予了用戶通過高級C++接口來包裝C++類和函數(shù)的能力,Boost.Python也在如下領(lǐng)域超越了早期的系統(tǒng):

  • C++虛函數(shù)支持,虛函數(shù)可以用Python來覆蓋(override)
  • 在整個生命周期內(nèi)對低級指針和引用進(jìn)行全面管理的設(shè)施
  • 對把擴(kuò)展(extensions)組織成Python packages的支持,通過中心注冊表(central registry)來進(jìn)行語言間類型轉(zhuǎn)換
  • 安全而便利的連接強(qiáng)大的Python序列化引擎(pickle)的機(jī)制
  • C++的lvalue和rvalue的一致的處理規(guī)則,這只能來自對Python和C++兩者的類型系統(tǒng)的深入理解。

鼓舞Boost.Python開發(fā)的關(guān)鍵發(fā)現(xiàn)是,傳統(tǒng)擴(kuò)展開發(fā)中的大量樣板代碼都可以通過C++編譯時內(nèi)省來消除。被包裝的C++函數(shù)的每個參數(shù)都必須根據(jù)參數(shù)類型從Python對象里取出來。類似地,函數(shù)返回值的類型決定了返回值如何從C++轉(zhuǎn)換成Python。參數(shù)類型和返回值類型當(dāng)然都是每個函數(shù)的類型的一部分,正是從這里,Boost.Python推導(dǎo)出了大部分需要的信息。

這種方法導(dǎo)向了 用戶引導(dǎo)的包裝 :盡可能的用純C++的框架直接從要包裝的代碼里取得信息,這以外的信息由用戶顯式提供。大多數(shù)引導(dǎo)是自動的,很少需要真正的干涉。因?yàn)閷懡涌谝?guī)范和寫被暴露的代碼的是同一門全功能語言,當(dāng)需要取得控制時用戶有了空前強(qiáng)大的能力。

Boost.Python 設(shè)計(jì)目標(biāo)

Boost.Python的首要目標(biāo)是讓用戶只用C++編譯器就能向Python暴露C++類和函數(shù)。大體來講,允許用戶直接從Python操作C++對象。

然而,有一點(diǎn)很重要,那就是不要 過于 按字面翻譯所有接口:必須考慮每種語言的慣用語。例如,雖然C++和Python都有迭代器的概念,表達(dá)方式卻很不一樣。Boost.Python必須能連接這些不同的接口。

必須把Python用戶和C++接口的微小誤用造成的崩潰隔離。出于同樣原因,應(yīng)該把C++用戶和低級Python ‘C’ API隔離,容易出錯的C接口,比如手動引用計(jì)數(shù)管理,原始的(raw)PyObject指針,應(yīng)該用更加健壯的(more-robust)替代物來取代。

支持基于組件的開發(fā)是至關(guān)重要的,因此被暴露在一個擴(kuò)展模塊里的C++類型應(yīng)該能夠被傳遞給被暴露在另一個模塊中的函數(shù),而不丟失重要的信息,比如說C++繼承關(guān)系。

最后,所有的包裝必須是 非侵入的(non-intrusive) ,不能修改甚至看不到原始的C++代碼。對只能看見它頭文件和二進(jìn)制文件的第三方,現(xiàn)有的C++庫必須是可包裝的。

Hello Boost.Python World

現(xiàn)在來預(yù)覽一下Boost.Python,并看看它如何改進(jìn)Python的原始包裝功能。下面是我們想暴露的一個函數(shù):

char const* greet(unsigned x)
{
   static char const* const msgs[] = { "hello","Boost.Python","world!" };

   if (x > 2)
       throw std::range_error("greet: index out of range");

   return msgs[x];
}

用Python的C API和標(biāo)準(zhǔn)C++來包裝這個函數(shù),我們需要像這樣:

extern "C" // 所有Python交互都使用C鏈接和調(diào)用習(xí)慣
{
    // 處理參數(shù)/結(jié)果轉(zhuǎn)換和檢查的包裝層
    PyObject* greet_wrap(PyObject* args,PyObject * keywords)
    {
         int x;
         if (PyArg_ParseTuple(args,"i",&x))    // 取出/檢查參數(shù)
         {
             char const* result = greet(x);      // 調(diào)用被包裝的函數(shù)
             return PyString_FromString(result); // 結(jié)果轉(zhuǎn)換成Python
         }
         return 0;                               // 發(fā)生了錯誤
    }

    // 待包裝函數(shù)表,函數(shù)用這個模塊來暴露
    static PyMethodDef methods[] = {
        { "greet",greet_wrap,METH_VARARGS,"return one of 3 parts of a greeting" }
        ,{ NULL,NULL,0,NULL } // sentinel
    };

    // 模塊初始化函數(shù)
    DL_EXPORT init_hello()
    {
        (void) Py_InitModule("hello",methods); // 添加成員函數(shù)(method)到模塊
    }
}

現(xiàn)在看看我們使用Boost.Python來暴露它時的包裝代碼:

#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
    def("greet",greet,"return one of 3 parts of a greeting");
}

下面是使用它的代碼:

>>> import hello

>>> for x in range(3):
...     print hello.greet(x)
...
hello
Boost.Python
world!

C API版本要冗長的多,此外,一些它沒有正確處理的地方值得提到:

  • 原來的函數(shù)接受無符號整數(shù),Python ‘C’ API僅僅提供了提取有符號整數(shù)的方式。如果我們試圖向hello.greet傳遞負(fù)數(shù)Boost.Pyt hon版將拋出Python異常,而Python ‘C’ API版則會繼續(xù)像在C++實(shí)現(xiàn)中那樣轉(zhuǎn)換負(fù)數(shù)到無符號數(shù)(通常包裝成某種很大的數(shù)),然后 把不正確的轉(zhuǎn)換結(jié)果傳給被包裝函數(shù)。
  • 這引起了第二個問題:如果函數(shù)的參數(shù)大于2,C++ greet()被調(diào)用時會拋出異常。典型的,如果C++異常跨越C編譯器生成的代碼的 邊界傳遞,會導(dǎo)致崩潰。正如你在第一個版本中看到的,那兒沒有C++腳手架(scaffolding)來防止崩潰發(fā)生。被Boost.Python包裝 過的函數(shù)自動包含了異常處理層,它把未處理的C++異常翻譯成相應(yīng)的Python異常,從而保護(hù)了Python用戶。
  • 一個更微妙的限制是,Python ‘C’ API的參數(shù)轉(zhuǎn)換機(jī)制只能以一種方式取得整數(shù)x。如果一個Python long 對象(任意精度整數(shù)) 碰巧可以轉(zhuǎn)換成(fit in)unsigned int而不能轉(zhuǎn)換成signed long,PyArg_ParseTuple不能對其進(jìn)行轉(zhuǎn)換。同樣如果被包裝的C++ 類包含用戶定義的隱式operator unsigned int()轉(zhuǎn)換,它永遠(yuǎn)不能處理。Boost.Python的動態(tài)類型轉(zhuǎn)換注冊表允許用戶任意添加 轉(zhuǎn)換方法。

庫概覽

這部分簡要描述了庫的主要特性。為了避免混淆,忽略了實(shí)現(xiàn)細(xì)節(jié)。

暴露類(Exposing Classes)

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'

構(gòu)造器(Constructors)

因?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++重載。

數(shù)據(jù)成員和屬性(Properties)

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")
     ...

這樣有兩個效果:

  1. 當(dāng)class_<…>被創(chuàng)建時,在Boost.Python的注冊表里查找對應(yīng)于Base1和Base2的Python類型對象,然后作為新的Python衍生類 型對象的基類。因而暴露給Base1和Base2的成員函數(shù)自動成為了衍生類型的成員。因?yàn)樽员硎侨值模词贡┞堆苌愋偷哪?塊和它的任一基類的模塊不同,繼承同樣有效。
  2. 衍生類到基類的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__(…)來糾正錯誤。

虛函數(shù)

在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)了。

更深層次的反射即將出現(xiàn)(Deeper Reflection on the Horizon)?

無可否認(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")他們自己的代碼。

序列化(Serialization)

序列化是把內(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é))在保持代碼便于管理上幫了大忙。

對象接口(Object interface)

無所不在的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ù)。

從混合的思路思考(Thinking hybrid)

因?yàn)榻Y(jié)合編程語言具有實(shí)際的和心理的困難,在進(jìn)行任何實(shí)際開發(fā)前決定使用單一的語言是普遍現(xiàn)象。對很多應(yīng)用來說,性能上的考慮決定了核心算法要用編譯語言實(shí)現(xiàn)。不幸的是,因?yàn)殪o態(tài)類型系統(tǒng)的復(fù)雜性,我們?yōu)檫\(yùn)行時性能要付出開發(fā)時間大量增長的代價。經(jīng)驗(yàn)表明,和開發(fā)相應(yīng)的Python代碼比起來,開發(fā)可維護(hù)的C++代碼通常需要更長的時間和艱難得多才能獲得的工作經(jīng)驗(yàn)。即使開發(fā)者覺得只用一門編譯語言開發(fā)挺好,為了用戶,他們也經(jīng)常用某種類型的特別的腳本層來補(bǔ)充系統(tǒng),哪怕他們永遠(yuǎn)不會得到同樣的好處。

Boost.Python讓我們能 think hybrid 。Python可以用來快速搭建新的應(yīng)用的原型;在開發(fā)能工作的系統(tǒng)時,它的易用性和大量的標(biāo)準(zhǔn)庫使我們處于領(lǐng)先。需要的話,可以用能工作的代碼來找出限制速度的熱點(diǎn)(rate-limiting hotspots)。為了最大化性能,這些熱點(diǎn)可以用C++來重新實(shí)現(xiàn),然后用Boost.Python綁定來連進(jìn)已有的高級過程(譯注:指Python程序)。

當(dāng)然,如果一開始就清楚很多算法最后不得不用C++來實(shí)現(xiàn),這種 由頂至下(top-down) 的方法就沒那么吸引人了。幸運(yùn)的是,Boost.Python也允許我們使用 由底至上(bottom-up) 的方法。我們非常成功的把這種方法用在了一個用于科學(xué)應(yīng)用的工具箱軟件的開發(fā)上。這個工具箱開始主要是一個帶Boost.Python綁定的C++類庫,接下來有一小段時間增長主要集中在C++部分,隨著工具箱變得越來越完整,越來越多新添加的功能可以用Python來實(shí)現(xiàn)。

http://static.flickr.com/55/124987534_34375196e6.jpg?v=0

上圖是實(shí)現(xiàn)新的算法時新添加的C++代碼和Python代碼的估計(jì)比率隨時間變化的情況。我們預(yù)計(jì)這個比率會達(dá)到接近70% (Python)。能夠主要用Python而不是更困難的靜態(tài)類型語言來解決新問題,這是我們在Boost.Python上的投入的回報。我們的所有代碼都能從Python訪問,這使得更廣泛的開發(fā)者可以用它來快速開發(fā)新的應(yīng)用。

開發(fā)歷史

Boost.Python的第一版是由Dave Abrahams在Dragon Systems開發(fā)的。在那里他非常榮幸的請到Tim Peters作為他的"Python之禪"( "The Zen of Python")導(dǎo)師。Dave的工作之一是開發(fā)基于Python的自然語言處理系統(tǒng)。因?yàn)樽罱K要被用于嵌入式硬件,系統(tǒng)計(jì)算密集的內(nèi)核總是被假設(shè)成要用C++來重寫以優(yōu)化速度和內(nèi)存需求量(memory footprint) [1] 。這個項(xiàng)目也想用Python測試腳本 [2] 來測試所有的C++代碼。當(dāng)時我們知道的綁定C++和Python的唯一工具是 SWIG ,但那時它處理C++的能力比較弱。要說在那時就對Boost.Python所使用方法的可能優(yōu)越之處有了什么深刻洞見,那是騙人的。Dave對花俏的C++模板技巧的興趣和嫻熟剛好到了能真正做點(diǎn)什么的時候,Boost.Python就那樣出現(xiàn)了,因?yàn)樗鼭M足了需求,因?yàn)樗雌饋硗幔档靡辉嚒?/p>

早期的版本針對的許多基本目標(biāo)和在這篇論文中描述的相同。最顯著的區(qū)別在于早期版本的語法要稍微麻煩一點(diǎn),缺乏對操作符重載,pickling,基于組件的開發(fā)的專門支持。后面三個特性很快就被Ullrich Koethe和Ralf Grosse-Kunstleve [3] 加上了。其他熱心的貢獻(xiàn)者(contributors)也出來貢獻(xiàn)了一些改進(jìn),如對嵌套模塊和成員函數(shù)的支持等。

到2001年早期時開發(fā)已經(jīng)穩(wěn)定下來了,很少有新的特性添加,然而這時一個煩人的問題暴露出來了:Ralf已經(jīng)開始在一個使用 EDG 前端的編譯器的預(yù)發(fā)布版上測試Boost.Python,這時Boost.Python內(nèi)核中負(fù)責(zé)處理Python和C++的類型轉(zhuǎn)換的機(jī)制(mechanism)編譯失敗了。結(jié)果證明我們一直在利用一個bug,這個bug在所有我們測試過的C++編譯器實(shí)現(xiàn)中都非常普遍。我們知道隨著C++編譯器很快變得更加標(biāo)準(zhǔn)兼容,庫將開始在更多的平臺上失敗。很不幸,因?yàn)檫@套機(jī)制是庫的功能的中樞,解決問題看起來非常困難。

幸運(yùn)的是那一年的后期,Lawrence Berkeley和后來的Lawrence Livermore National labs和 Boost Consulting 簽訂了支持Boost.Python的開發(fā)的合同。這樣就有了新的機(jī)會來處理庫的基本問題,從而確保將來的發(fā)展。重新設(shè)計(jì)開始于低級類型轉(zhuǎn)換架構(gòu),內(nèi)置的標(biāo)準(zhǔn)兼容和對基于組件的開發(fā)的支持(和不得不顯式的跨越模塊邊界導(dǎo)入或?qū)С鲛D(zhuǎn)換的第一版形成對比)。對Python和C++對象的關(guān)系進(jìn)行了分析,從而能更直觀的處理C++ lvalues和rvalues。

Python 2.2里出現(xiàn)的強(qiáng)大的新類型系統(tǒng)使得選擇是否維護(hù)對Python 1.5.2的兼容性變得容易了:這個丟棄大量精心制作的僅僅用來模擬classic Python類的代碼的機(jī)會,好的令人無法拒絕。另外,Python iterators 和 descriptors提供了重要且優(yōu)雅的工具來描述類似的C++結(jié)構(gòu)。一般化了的對象接口的開發(fā)允許我們進(jìn)一步把C++程序員和使用Python C API帶來的危險性和語法負(fù)擔(dān)隔離開。大量的其他特性在這個階段被加了進(jìn)來,包括C++異常翻譯,改進(jìn)的重載函數(shù)支持,還有最重要的用來處理指針和引用的CallPolicies。

于2002年十月,第二版的Boost.Python發(fā)布了。那以后的開發(fā)集中在改進(jìn)對C++運(yùn)行時多態(tài)和智能指針的支持上。特別是Peter Dimov的巧妙的boost::shared_ptr設(shè)計(jì)使我們能給混和系統(tǒng)開發(fā)者提供一致的接口,用于跨越語言藩籬來回移動對象而不丟失信息。

剛開始,我們擔(dān)心Boost.Python v2的復(fù)雜性會阻礙貢獻(xiàn)者,但 Pyste 和幾個其他重要特性的貢獻(xiàn)(contribution)的出現(xiàn)使這些擔(dān)心顯得多余了。每天出現(xiàn)在Python C++-sig上的問題和希望得到的改進(jìn)的積壓(backlog)表明了庫正在被使用。對我們來說,未來看起來很光明。

結(jié)論

Boost.Python 實(shí)現(xiàn)了兩種功能豐富的優(yōu)秀的語言環(huán)境間的無縫協(xié)作。因?yàn)樗媚0逶幊碳夹g(shù)來對類型和函數(shù)進(jìn)行內(nèi)省,用戶永遠(yuǎn)用不著再學(xué)第三種語言:接口定義是用簡潔而可維護(hù)的C++寫的。同樣,包裝系統(tǒng)不用再解析C++頭文件或是描述類型系統(tǒng):編譯器都給我們做了。

計(jì)算密集的任務(wù)適合強(qiáng)大的C++,它一般不可能用純Python來實(shí)現(xiàn)。然而像序列化這樣的工作,可能用Python很簡單,用C++就非常困難。假如有從底層開始構(gòu)建混合系統(tǒng)的奢侈,我們有新的信心和動力來進(jìn)行設(shè)計(jì)。