使用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来完成本教程。

猜你喜欢

转载自blog.csdn.net/sunlin972913894/article/details/106981382