使用Boost::Python在C++應用程序中嵌入Python

使用Boost::Python在C++應用程序中嵌入Python:第一部分

翻譯: Leon Lee([email protected])
原文:在此

在本系列教程的簡介中,我說了將Python代碼集成到Granola代碼庫中的動機。簡而言之,它可以使我使用Python語言和標準庫的好處來完成在C++中通常很痛苦或笨拙的任務。當然,底線是我不必移植任何已有的C++代碼。

今天,我們看一下使用boost::python在C++中嵌入Python並與Python對象交互的基本步驟。我已將此部分中的所有代碼放在github倉庫中,請隨意檢出代碼並使用。

從Python的內核來說,嵌入Python非常簡單,不需要任何C++代碼--Python發行版提供的庫中包括C綁定內容。我們將跳過所有這些,直接進入通過boost::python在C++中使用Python,它提供了類包裝和多態行爲,相比C綁定,更與實際Python代碼一致、本教程後面的部分,我們將介紹一些無法通過boost::python做到的事情(特別是多線程和錯誤處理)

好了,要開始的話,首先需要下載並構建boost,或者在包管理器得到一份副本。如果你選擇構建,你可以只構建boost::python庫(可惜不只是頭文件),但是如果你經常使用C++編程,我還是建議熟悉整個boost庫。如果你已經同步了上面的git倉庫,確保在Makefile裏把路徑指向你的boost安裝目錄。好了,我們繼續。

首先,我們需要能夠構建嵌入Python的應用程序。使用gcc這不是很困難,它只是將boost::python和libpython以靜態或者共享庫的方式包含進來。根據你構建boost的方式不同,你可能會遇到各種困難。在github上的教程代碼裏,我們使用靜態的boost::python庫(libboost_python.a)和Python庫的動態版本(libpython.so)。

我在MiserWare的開發工作的一個軟性要求是使我們所有支持操作系統(一些Windows和一系列不斷變化的Linux發行版)的環境保持一致。因此Granola鏈接到固定的Python版本,安裝的版本里包括了運行代碼所需要的Python庫文件。也許並不理想,但是它提供了一個我肯定我們的代碼將在所有支持的操作系統上運行的環境。

讓我們運行一些代碼。可以想象,可能需要包含正確的頭文件。

Py_Initialize();

py::object main_module = py::import(“__ main__”);

py::object main_namespace = main_module.attr(“__ dict__”);

注意,你必須直接初始化Python解釋器(第一行)。雖然boost::python極大的簡化了嵌入Python的任務,但是它並不能處理你需要做的所有事情。正如前面提到的,我們將在接下來的教程裏看到更多的缺陷。在初始化以後,__main__模塊被導入,命名空間被解析,這將產生空白的運行環境,我們可以在上面調用Python代碼,添加模塊和變量。

boost::python::exec("print 'Hello, world'", main_namespace);

boost::python::exec("print 'Hello, world'[3:5]", main_namespace);

boost::python::exec("print '.'.join(['1','2','3'])", main_namespace);

exec函數在指定的命名空間內運行字符串參數中的代碼。所有正常的、未導入的代碼都可以。當然,由於不能導入模塊和提取值,因此不是很有用。

boost::python::exec("import random", main_namespace);

boost::python::object rand = boost::python::eval("random.random()", main_namespace);

std::cout << py::extract<double>(rand) << std::endl;

這裏我們在命名空間__main__裏通過執行相應的Python語句來導入random模塊,把這個模塊帶入這個命名空間。當模塊可用後,我們可以在這個命名空間裏使用函數、對象和變量。本例裏,我們使用了eval函數,它返回傳入的Python語句的運行結果,來創建一個boost::python對象來包含random模塊的random()函數返回的隨機值。最後,我們將值以C++ double類型提取並打印出來。

這可能看上去有點......軟。通過將格式化的Python字符串傳遞給C++函數來調用Python?這不是以一種非常面向對象的方式來處理事務。幸運的是,有一種更好的辦法。

boost::python::object rand_mod = boost::python::import("random");

boost::python::object rand_func = rand_mod.attr("random");

boost::python::object rand2 = rand_func();

std::cout << boost::python::extract(rand2) << std::endl;

在這個最後的例子裏,我們導入了random模塊,但這次我們使用的是boost::python的import函數,它把模塊加載到boost python的對象中。接下來,random函數對象從random模塊中提取出來並存儲在boost::python對象中。調用該函數,返回一個包含隨機數的Python對象。最後double值被提取和打印出來。通常,所有Python對象都可以以這種方式處理--函數、類、內置類型。

當你開始持有複雜的標準庫對象和用戶定義類的實例時,它開始變得有趣。接下來的教程,我將按部就班圍繞ConfigParser模塊構建一個真正的配置解析類討論從C++代碼解析Python異常。

使用Boost::Python在C++應用程序中嵌入Python:第二部分

第1部分中,我們瞭解瞭如何在C++應用程序中嵌入Python,包括從應用程序調用Python代碼的幾種方法。雖然我之前承諾在第2部分中完整實現一個配置解析器,但我認爲看一下錯誤解析會更有建設性。一旦我們有一個很好的方法來處理Python代碼中的錯誤,我將在第3部分中創建承諾的配置解析器。我們開始吧!

如果您獲得了本教程git repo副本並且正在使用它,您可能已經體驗過boost::python處理Python錯誤的方式-- error_already_set異常類型。如果沒有,以下代碼將生成異常:

namespace py = boost::python;

...

Py_Initialize();

...

py::object rand_mod = py::import("fake_module");

…它的輸出不是那麼有用:

terminate called after throwing an instance of 'boost::python::error_already_set'

Aborted

簡而言之,boost::python處理的Python代碼中發生的任何錯誤都會導致庫拋出此異常; 遺憾的是,該異常並未封裝有關錯誤本身的任何信息。要提取有關錯誤的信息,我們將不得不求助於使用Python C API和一些Python本身的機制。首先,捕捉錯誤:

try{

Py_Initialize();

py::object rand_mod = py::import("fake_module");

}catch(boost::python::error_already_set const &){

std::string perror_str = parse_python_exception();

std::cout << "Error in Python: " << perror_str << std::endl;

}

這裏,我們調用parse_python_exception函數來提取錯誤字符串並將其打印出來。如此所示,異常數據靜態存儲在Python庫中,而不是封裝在異常本身中。parse_python_exception函數的第一步是使用Python C API的PyErr_Fetch函數提取該數據:

std::string parse_python_exception(){

PyObject *type_ptr = NULL, *value_ptr = NULL, *traceback_ptr = NULL;

PyErr_Fetch(&type_ptr, &value_ptr, &traceback_ptr);

std::string ret("Unfetchable Python error");

...

由於可能存在全部、部分或沒有異常數據,我們使用回退值設置返回的字符串。接下來,我們嘗試從異常信息中提取和字符串化類型數據:

...

if(type_ptr != NULL){

py::handle<> h_type(type_ptr);

py::str type_pstr(h_type);

py::extract<std::string> e_type_pstr(type_pstr);

if(e_type_pstr.check())

ret = e_type_pstr();

else

ret = "Unknown exception type";

}

...

在這個塊中,我們首先檢查是否真有一個指向類型數據的有效指針。如果存在,我們構造一個boost::python::handle指向該數據,然後我們從中創建一個str對象。此轉換應確保可以進行有效的字符串提取,但要進行雙重檢查,我們創建一個提取對象,檢查對象,然後在有效的情況下執行提取。否則,我們使用回退字符串作爲類型信息。

接着,我們對異常值執行非常類似的步驟:

...

if(value_ptr != NULL){

py::handle<> h_val(value_ptr);

py::str a(h_val);

py::extract<std::string> returned(a);

if(returned.check())

ret += ": " + returned();

else

ret += std::string(": Unparseable Python error: ");

}

...

我們將值字符串附加到現有錯誤字符串。對於大多數內置異常類型,值字符串是描述錯誤的可讀字符串。

最後,我們提取回溯數據:

if(traceback_ptr != NULL){

py::handle<> h_tb(traceback_ptr);

py::object tb(py::import("traceback"));

py::object fmt_tb(tb.attr("format_tb"));

py::object tb_list(fmt_tb(h_tb));

py::object tb_str(py::str("\n").join(tb_list));

py::extract<std::string> returned(tb_str);

if(returned.check())

ret += ": " + returned();

else

ret += std::string(": Unparseable Python traceback");

}

return ret;

}

回溯類似於類型和值提取,除了將回溯對象格式化爲字符串的額外步驟。爲此,我們導入traceback模塊。從traceback中,我們然後提取format_tb函數並使用traceback對象的句柄調用它。這會生成一個回溯字符串列表,然後我們將它們連接成一個字符串。也許不是最漂亮的輸出,但它完成了工作。最後,我們如上所述提取C ++字符串類型,並將其附加到返回的錯誤字符串並返回整個結果。

在前面錯誤的上下文中,應用程序現在生成以下輸出:

Error in Python: : No module named fake_module

As I mentioned above, in Part 3 I will walk through the implementation of a configuration parser built on top of the ConfigParser Python module. Assuming, of course, that I don't get waylaid again.

一般來說,這個函數可以更容易地找到嵌入Python代碼中問題的根本原因。需要注意的是:如果您正在爲嵌入解釋器配置自定義Python環境(尤其是模塊路徑),則該parse_python_exception函數本身可能在嘗試加載traceback模塊時拋出一個boost::error_already_set異常,因此您可能希望將對函數的調用包裝到try...catch塊中並解析結果中的類型和值指針。

如上所述,在第3部分中,我將介紹構建在ConfigParserPython模塊之上的配置解析器的實現。當然,假設我沒有再次中斷。

使用Boost::Python在C++應用程序中嵌入Python:第三部分

本教程的第2部分中,我介紹了一種方法,使用應用程序的C++代碼處理嵌入的Python代碼拋出的異常。這對於調試嵌入式Python代碼至關重要。在本教程中,我們將創建一個簡單的C++類,它利用Python功能來處理開發實際應用程序中經常令人煩惱的部分:配置解析。

爲了不讓C++精英們感到憤怒,我將以外交方式說出這一點:我在C++中使用複雜的字符串操作。STL stringsstringstreams極大簡化了任務,但執行應用程序級任務,並以健壯的方式執行它們,總是導致我編寫更多的代碼。因此,我最近使用嵌入Python,特別是ConfigParser模塊,重新編寫了Granola Connect(Granola Enterprise中用於處理與Granola REST API通信的守護進程)的配置解析機制。

當然,字符串操作和配置解析只是一個例子。對於第3部分,我可以選擇任何數量的C++難以處理而Python中很簡單的任務(例如,Web連接),但是配置解析類是一個簡單但完整的用於嵌入Python以供實際使用的示例。從Github repo中獲取本教程的代碼。

首先,讓我們創建一個涵蓋非常基本的配置解析的類定義:讀取和解析INI樣式的文件,提取給定名稱和節的字符串值,併爲給定的節設置字符串值。這是類聲明:

class ConfigParser{

private:

boost::python::object conf_parser_;


void init();

public:

ConfigParser();


bool parse_file(const std::string &filename);

std::string get(const std::string &attr,

const std::string &section = "DEFAULT");

void set(const std::string &attr,

const std::string &value,

const std::string &section = "DEFAULT");

};

ConfigParser模塊提供的功能遠遠超出本教程所涵蓋的功能,但我們在此實現的子集應作爲實現更復雜功能的模板。該類的實現相當簡單; 首先,構造函數加載__main__模塊,提取字典,將ConfigParser模塊導入命名空間,並創建一個boost::python::object類型的成員變量來包含RawConfigParser對象:

ConfigParser::ConfigParser(){

py::object mm = py::import("__main__");

py::object mn = mm.attr("__dict__");

py::exec("import ConfigParser", mn);

conf_parser_ = py::eval("ConfigParser.RawConfigParser()", mn);

}

用以下config_parser_對象執行文件解析以及值的獲取和設置:

bool ConfigParser::parse_file(const std::string &filename){

return py::len(conf_parser_.attr("read")(filename)) == 1;

}


std::string ConfigParser::get(const std::string &attr, const std::string &section){

return py::extract<std::string>(conf_parser_.attr("get")(section, attr));

}


void ConfigParser::set(const std::string &attr, const std::string &value, const std::string &section){

conf_parser_.attr("set")(section, attr, value);

}

在這個簡單的例子中,爲了簡潔起見,允許傳播異常。在更復雜的環境中,您幾乎肯定希望讓C++類處理並將Python異常重新打包爲C++異常。如果性能或其他問題成爲問題,您可以稍後創建一個純C++類。

要使用該類,調用代碼可以簡單地將其視爲普通的C++類:

int main(){

Py_Initialize();

try{

ConfigParser parser;

parser.parse_file("conf_file.1.conf");

cout << "Directory (file 1): " << parser.get("Directory", "DEFAULT") << endl;

parser.parse_file("conf_file.2.conf");

cout << "Directory (file 2): " << parser.get("Directory", "DEFAULT") << endl;

cout << "Username: " << parser.get("Username", "Auth") << endl;

cout << "Password: " << parser.get("Password", "Auth") << endl;

parser.set("Directory", "values can be arbitrary strings", "DEFAULT");

cout << "Directory (force set by application): " << parser.get("Directory") << endl;

// Will raise a NoOption exception

// cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;

}catch(boost::python::error_already_set const &){

string perror_str = parse_python_exception();

cout << "Error during configuration parsing: " << perror_str << endl;

}

}

就是這樣:一個包含塊和註釋的鍵值配置解析器只需要50行代碼。這只是冰山一角。在幾乎相同長度的代碼中,您可以執行各種各樣的事情,這些事情在C++中最爲痛苦,更容易出錯且耗時:配置解析、列表和集合操作、Web連接、文件格式操作(想想XML/JSON),以及無數其他已在Python標準庫中實現的任務。

第4部分中,我將介紹如何使用仿函數和Python命名空間的類來更強大和通用地調用Python代碼。

使用Boost::Python在C++應用程序中嵌入Python:第四部分

在這個教程的第2部分中,我介紹了用於從C++解析Python異常的代碼。在第3部分中,我使用Python ConfigParser模塊實現了一個簡單的配置解析類。作爲該實現的一部分,我提到對於任何規模的項目,人們都希望在類中捕獲並處理Python異常,以便該類的客戶不必瞭解Python的細節。從調用者的角度來看,這個類就像任何其他C++類一樣。

處理Python異常的明顯方法是在每個函數中處理它們。例如,我們創建的C++ ConfigParser類的get函數將變爲:

std::string ConfigParser::get(const std::string &attr, const std::string &section)
{

    try{

        return py::extract(conf_parser_.attr("get")(section, attr));

    }catch(boost::python::error_already_set const &){

        std::string perror_str = parse_python_exception();

        throw std::runtime_error("Error getting configuration option: " + perror_str);

    }

}

錯誤處理代碼保持不變,但現在main函數變爲:

int main()
{

    Py_Initialize();

    try
    {

        ConfigParser parser;

        parser.parse_file("conf_file.1.conf");

        ...
    
        // Will raise a NoOption exception

        cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;

    }catch(exception &e){

    cout << "Here is the error, from a C++ exception: " << e.what() << endl;

    }

}

當Python異常被拋出時,它將被解析並重新打包爲一個std::runtime_error,它在調用處被捕獲並像正常的C++異常一樣處理(即無需經歷parse_python_exception嚴格的操作)。對於只有少數函數或使用嵌入式Python的一兩個類的項目,這肯定會有效。但是,對於更大的項目,人們希望避免大量重複的代碼,它將不可避免地帶來的錯誤。

對於我的實現,我想總是以相同的方式處理錯誤,但我需要一種方法來調用具有不同簽名的不同函數。我決定利用boost庫的另一個強大的領域:仿函數庫,特別是boost::bindboost::functionboost::function提供仿函數類包裝器,boost::bind綁定函數的參數。然後,這兩者一起啓用函數及其參數的傳遞,這些函數及其參數可以在以後調用。正是醫生所要求的!

要使用仿函數,函數需要知道返回類型。由於我們使用不同的簽名包裝函數,因此函數模板可以很好地完成這一操作:

template <class return_type>

return_type call_python_func(boost::function<return_type ()> to_call, const std::string &error_pre)
{
    std::string error_str(error_pre);
    try{
        return to_call();
    }catch(boost::python::error_already_set const &)
    {
        error_str = error_str + parse_python_exception();
        throw std::runtime_error(error_str);
    }
}

此函數將仿函數對象作爲調用boost::python函數的函數。每個調用boost::python代碼的函數現在被分成兩個函數:私有的核心函數調用Python功能,公開的包裝的函數使用call_python_func函數。這是更新的get函數及其合作伙伴:

string ConfigParser::get(const string &attr, const string &section)
{
    return call_python_func<string>(boost::bind(&ConfigParser::get_py, this, attr, section), "Error getting configuration option: ");
}

string ConfigParser::get_py(const string &attr, const string &section)
{
    return py::extract<string>(conf_parser_.attr("get")(section, attr));
}

get函數將傳入的參數與隱式this指針綁定到get_py函數,get_py·函數又調用boost::python`執行操作所需的函數。簡單有效。

當然,這裏有一個權衡。不是重複的try...catch塊代碼和Python錯誤處理,每個類聲明的函數數量增加了一倍。出於我的目的,我更喜歡第二種形式,因爲它更有效地利用編譯器來發現錯誤,但長度可能會有所不同。最重要的一點是在理解Python的代碼級別處理Python錯誤。如果你的整個應用程序需要理解Python,你應該考慮用Python重寫而不是嵌入,也許根據需要使用一些C++的模塊。

與往常一樣,您可以通過克隆github repo來完成本教程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章