目录
???????说明
嵌入 Python
编写嵌入 Python 程序
?C++ 调用 Python 代码
导入 Python 模块
执行 Python 文件
操作 Python 对象
运行传入的 Python 方法
处理异常
关于命名空间变量
handle 类详解
exec 详解
Python C API
完整示例
参考链接
说明
Boost.Python 不仅可以提供将 C++ 代码封装成 Python 接口供 Python 程序调用的功能,反过来,也可以使用 Boost.Python 在 C++ 程序中调用 Python 方法。但是需要注意的是,截至当前版本的 Boost(1.79) 的这一功能还不完善,需要与 Python/C API 结合使用。官方说法是后续可能会完善,但是当前还不能独立使用。
先总结一下个人想法吧,总体看下来,Boost.Python 的嵌入 Python 方案其实现完全依靠调用 Python/C API 的内容,只是在后者基础之上做了一层封装,使得看起来更加便利了而已。对于开发者来说,如果仅仅为了实现嵌入 Python 的功能,没有必要使用 Boost 库,但是对于依赖 Boost 的软件项目来说,使用 Boost.Python 确实能简化不少流程,且能降低这方面出错的概率。
嵌入 Python
Boost.Python 包含两种模型 extending 和 embedding。前者是将 C++ 编写的库封装成 Python 接口时使用的,后者则用于在 C++ 程序中调用 Python 解释器作为库子例程,一般为现有应用程序添加脚本功能。两者的主要区别在于 C++ main() 函数的位置,分别在 Python 解释器可执行文件或其他程序中。
我们将在 C/C++ 代码调用的 Python 语句称为 Embedding Python。
注:关于 extending 和 embedding,我们不必太在意细节,对于软件开发人员来说,他们在使用上没有任何区别。通常能使用 Boost.Python 库参与编译,这两者都是可用的。
编写嵌入 Python 程序
使用 Boost.Python 编写嵌入 Python 程序一般遵循以下4个步骤:
- 包含头文件 <boost/python.hpp>
- 调用 Py_Initialize() 来初始化 Python 解释器并创建 __main__ 模块
- 调用其他 Python C API 来使用 Python 解释器
- 编写自己的业务代码
注:在当前为止的版本(1.79),我们需要手动调用 Py_Finalize() 来停止 Python 解释器,在将来的版本中会解决这个问题,但是在当前还是需要。
先看一个简单的示例:
#include <boost/python.hpp>
using namespace boost::python;
int main( int argc, char ** argv ) {
try {
Py_Initialize();
// 方法一: 使用 Python C API 打印
/*
object main_module((
handle<>(borrowed(PyImport_AddModule("__main__")))));
object main_namespace = main_module.attr("__dict__");
handle<> ignored(( PyRun_String( "print(\"Hello, World\")",
Py_file_input,
main_namespace.ptr(),
main_namespace.ptr() ) ));
*/
// 方法二:使用 Boost.Python 接口打印
object ignored = exec("print(\"Hello, World\")");
Py_Finalize();
} catch( error_already_set ) {
PyErr_Print();
}
}
CMakeLists.txt
set(MODULE_NAME embedding)
add_executable(${MODULE_NAME} hello.cpp)
target_link_libraries(${MODULE_NAME}
${Boost_LIBRARIES}
${PYTHON_LIBRARIES}
)
在这个示例中实现了两种方法来打印 "Hello, World"。方法一主要使用 Python C API 的方法,二则使用 Boost.Python 提供的 exec 功能。两者实现上没什么区别,后者是也是用方法一实现的,是对 Python C API 的封装,但是提供了更完善的检查和引用计数的处理。所以,如果使用了 Boost.Python,建议使用方法二。一方面更简洁,另一方面也更安全。
我们知道 Python 中的对象是有引用计数的,虽然使用 Python C API 的 Python 对象也有引用计数,但两者还是有区别的,Python 的引用计数由 Python 自动管理,而 Python C API 的引用计数则要求用户手动管理。这就使得对引用计数的处理变得很麻烦,尤其当程序运行异常时尤其难以解决。针对这一问题,Boost.Python 提供了 handle 和 object 类模板来自动化管理引用计数。也就是上面的例子中的 handle<>, object 相关语句。
?C++ 调用 Python 代码
Boost.Python 提供了四个相关的函数来在 C++ 中调用 Python 代码:
// eval计算给定的表达式并返回结果值
object eval(str expression, object globals = object(), object locals = object())
// exec执行 code 指定的 Python 代码段(通常是一组语句)
object exec(str code, object globals = object(), object locals = object())
// exec_statement 执行 string 指定的 Python 语句
object exec_statement(str string, object global = object(), object local = object());
// exec_file执行给定文件中包含的代码
object exec_file(str filename, object globals = object(), object locals = object())
?注:上例中的 str 参数也可以使用 const char *代替。
eval(), exec() 和 exec_statement() 三个接口的参数完全一致,应用场景也很接近。区别在于其内部实现里调用的 PyRun_String() 方法的 start 参数分别是 Py_eval_input, Py_file_input 和 Py_single_input。详见:The Very High Level Layer。
由于 exec_statement() 只能执行一行 Python 语句,而 exec() 可以执行一到多行,所以我建议尽量使用后者。
globals 和 locals 分别对应于 PyRun_String() 的同名参数, Boost.Python 在这里做成有默认值的可缺省的参数。
在 Python 中,全局和局部参数是以字典的形式保存的,即{arg_name:arg_value},在大多数情况下,我们可以使用 __main__ 模块来获取这些参数。比如:
object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
object ignored = exec("hello = file('hello.txt', 'w')\n"
"hello.write('Hello world!')\n"
"hello.close()",
main_namespace);
这段代码会在当前目录创建一个名为 "hello.txt"的文件,其中包含 "Hello world!" 内容。
导入 Python 模块
通过 Boost.Python 导入 Python 模块有两种方式,先看一种简单的,直接将 Python 模块的操作直接作为 Python 语句的一部分,然后调用 exec() 来运行:
object ignored = exec("import sys\n"
"version = sys.version\n"
"print(version)\n", main_namespace);
ignored = exec("import sys\n"
"encode = sys.getdefaultencoding()\n"
"print(encode)\n", main_namespace);
这种方式简单,但是缺点也显而易见,对模块的操作全都集中在一段代码内还好,但如果操作需要分段进行,则每次都要重新导入模块,效率太低。
一种更灵活的方式是,导入一次模块后,作为一个 C++ 对象保留下来,在任意需要的地方随时调用。Boost.Python 提供的 import() 属性可以很好的用于这种场景,导入后的模块的方法和变量等都作为 C++ 对象的属性。该 import 语句用于导入 Python 模块的调用格式为:?
object import(str name)
name 表示要导入的 python 模块名称。
那么上面的示例就可以改写为以下内容:
object sys = import("sys");
object version_o = sys.attr("version");
std::string version_s = extract<std::string>(version_o);
std::cout << version_s << std::endl;
std::string encode = extract<std::string>(sys.attr("getdefaultencoding")());
std::cout << encode << std::endl;
注: import() 函数是 Boost.Python 对 PyImport_ImportModule() 函数的封装,其作用与 object main_module((
handle<>(borrowed(PyImport_AddModule("__main__"))))); 语句类似。建议直接使用 import()。
执行 Python 文件
在 C++ 中执行 Python 程序文件,直接使用 exec_file() 即可,本身没什么可说的。但是需要注意两点。
先看示例:
# embedding.py
number_x = 30
print ("This is embedded python file")
if __name__ == '__main__':
number_y = 20
print ("Py run itself.")
// embedding.cpp
int main( int argc, char ** argv )
{
try {
Py_Initialize();
dict global;
object result = exec_file("embedding.py", global, global);
int num_x = extract<int>(global["number_x"]);
// int num_y = extract<int>(global["number_y"]); // KeyError: 'number_y'
std::cout << num_x << std::endl;
Py_Finalize();
} catch( error_already_set ) {
PyErr_Print();
}
}
?编译后运行结果:
This is embedded python file
30
?需要注意的两点是:
- 虽然是执行 Python 文件,但仍然是以 module 的形式。.py 文件中, if __name__ == '__main__': 以下的内容都不会执行(当然,这是有前提的,后面会说明);
- .py 文件中设置的变量会被保存到 Boost.Python 字典类型的命名空间中。
操作 Python 对象
我在之前的文章提到过,Boost.Python 使用类来抽象 Python 对象,恰当地使用 object 类及其派生类可以完美地操作所有的 Python 对象。
以下示例用以说明这一事实:
object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
object ignored = exec("result = 5 ** 2", main_namespace);
int five_squared = extract<int>(main_namespace["result"]);
在这里,我们为 __main__ 模块的命名空间创建一个字典对象。然后我们将 5 平方分配给结果变量并从字典中读取该变量。实现相同结果的另一种方法是使用 eval 代替,它直接返回结果:
object result = eval("5 ** 2");
int five_squared = extract<int>(result);
接下来我们回到上一个程序,对 C++ 代码稍作修改:
object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
// dict global;
object result = exec_file("embedding.py", main_namespace, main_namespace);
int num_x = extract<int>(main_namespace["number_x"]);
int num_y = extract<int>(main_namespace["number_y"]); // KeyError: 'number_y'
std::cout << num_x << num_y << std::endl;
再次编译运行,结果如下:
This is embedded python file
Py run itself.
3020
和之前的程序相比,我们这一次在代码里导入了 __main__ 模块,__main__ 是 Python 程序的入口,我们在这里导入了 "__main__" 之后,Python 魔法函数 __name__ 就被赋值了 __main__,那么 .py 文件里的语句也就会被执行了。
运行传入的 Python 方法
在上面这几种方式之外,C++ 程序还可以运行由 Python 作为参数传递进来的方法,此时 C++ 和 Python 之间的交互较为频繁,先由 Python 调用 C++ 接口,再在 C++ 内运行 Python 方法。
具体内容参见:boost.python:在c++程序中调用python的函数
处理异常
如果在执行 Python 代码的过程当中发生异常,Boost.Python 会抛出 error_already_set 类,该类包含用于管理和转换 Python 和 C++ 异常的类型和函数。
try
{
object result = eval("5/0");
// execution will never get here:
int five_divided_by_zero = extract<int>(result);
}
catch(error_already_set const &)
{
// handle the exception in some way
}
error_already_set 异常类本身不携带任何信息。要了解有关发生的 Python 异常的更多信息,您需要在 catch 语句中使用 Python C API 的异常处理函数。这可以像调用 PyErr_Print() 将异常的回溯打印到控制台一样简单,或者将异常的类型与标准异常的类型进行比较:
catch(error_already_set const &)
{
if (PyErr_ExceptionMatches(PyExc_ZeroDivisionError))
{
// handle ZeroDivisionError specially
}
else
{
// print all other errors to stderr
PyErr_Print();
}
}
关于命名空间变量
嵌入 Python 是为了为我所用,而在前面的示例中,看似 C++ 调用了 Python 代码,但两者仍然是比较孤立的,C++ 内定义的一系列函数和变量等并不能为 Python 代码所使用,这样也起不到结合两者有点的作用。此时,命名空间就有文章可做了。
我们在调用 exec() 时传入了 main_space 作为全局变量,它是一个字典类型的变量,对应于 python 环境下的全局变量字典。格式为 ["name":object] ,也就是说几乎所有类型的 Python 对象都可以作为该变量的成员。当我们在 C++ 程序中初始化 Python 解释器后,main_space 下的成员可以作为 Python 目标的属性直接调用。
比如,我们在 C++ 下定义一个简单的类和函数:
class mainspace {
public:
int get_num() { return 10; }
};
void showtime()
{
std::cout << "show time for namespace" << std::endl;
}
然后在执行 Python 时将上述类和函数作为成员添加进 Python 全局变量:
object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
main_namespace["mainspace"] = class_<mainspace>("mainspace")
.def("get_num",&mainspace::get_num);
main_namespace["show_time"] = showtime;
object ignored = exec("ms = mainspace()\n"
"print(ms.get_num())\n"
"show_time()\n", main_namespace);
此时 Python 全局变量下有两个成员: "mainspace" : class mainspace 和 "show_time" : showtime()。 Python 可以直接使用这两个成员,运行结果如下:
$ ./embedding
10
show time for namespace
OK,前面我们是将类的定义传递到了 Python 空间,在 Python 运行时要先创建一个类实例,然后才能调用类的成员函数。如果在初始化 Python 解释器之前,我们就有一个类的实例,也可以直接将该实例引用到 Python 空间。
比如,先实例化类,再将该实例赋值给 main_space:
mainspace aaa;
...
main_namespace["mspace"] = ptr(&aaa);
然后在 Python 下就可以直接使用了:
object ignored = exec("print(mspace.get_num())\n", main_namespace);
于是,我们终于可以访问我们的 C++ 应用程序的数据了,但是所有的数据都在全局命名空间中,对于优秀的程序设计而言并不是一个好的做法。为了避免这种情况并提供适当的封装,我们应该将所有类和数据放入一个模块中。第一步是删除所有修改 main_namespace 对象的行,然后添加一个标准的 Boost 模块定义:
BOOST_PYTHON_MODULE(CppMod) {
class_<mainspace>("mainspace")
.def("get_num",&mainspace::get_num);
}
现在,在 main() 内部和调用 Py_Initialize() 之前,我们要调用 PyImport_AppendInittab( "CppMod", &initCppMod ); initCppMod 是由 BOOST_PYTHON_MODULE 宏创建的函数,用于初始化 Python 模块 CppMod。此时,我们的嵌入式 python 程序就可以调用 import CppMod,然后作为模块成员访问 mainspace。
handle 类详解
前面的内容提到,Boost.Python 使用 handle<> 来实现引用计数的自动管理。句柄本质上是一个指向 Python 对象类型的智能指针;它拥有一个 T* 类型的指针,其中 T 是它的模板参数。 T 必须是派生自 PyObject 的类型或初始 sizeof(PyObject) 字节与 PyObject 布局兼容的 POD 类型。在 Python/'C' API 和高级代码之间的边界处使用 handle<>;首选对象作为 Python 对象的通用接口。
handle 类定义在<boost/python/handle.hpp> 文件中,提供类模板句柄,一个用于管理引用计数的 Python 对象的智能指针。
namespace boost { namespace python
{
template <class T>
class handle
{
typedef T* (handle::* bool_type )() const;
public: // types
typedef T element_type;
public: // member functions
~handle();
template <class Y>
explicit handle(detail::borrowed<null_ok<Y> >* p);
template <class Y>
explicit handle(null_ok<detail::borrowed<Y> >* p);
template <class Y>
explicit handle(detail::borrowed<Y>* p);
template <class Y>
explicit handle(null_ok<Y>* p);
template <class Y>
explicit handle(Y* p);
handle();
template <typename Y>
handle(handle<Y> const& r);
handle(handle const& r);
T* operator-> () const;
T& operator* () const;
handle& operator=(handle const& r);
template<typename Y>
handle& operator=(handle<Y> const & r); // never throws
T* get() const; // 返回当前句柄
void reset(); // 重置handle类对象,此时句柄指向的地址为NULL
T* release(); // 返回当前句柄,然后将当前句柄清零
operator bool_type() const; // never throws
private:
T* m_p;
};
template <class T> struct null_ok;
namespace detail { template <class T> struct borrowed; }
}}
handle 的构造函数和析构函数实现上依赖于 Python/C API 的 Py_XINCREF() 和 Py_XDECREF() 等操作,用于实现引用计数的加1减1操作。
构造函数的使用格式如下:
// 创建一个引用计数 +1 的 NULL 指针 x
handle<> x;
handle<> x = handle<> ();
// 创建一个基于对象 y 的带有引用计数的指针, null_ok 表示 y 可以为 NULL
handle<> x(y);
handle<> x(null_ok(y));
handle<> x = handle<> (y);
handle<> x = handle<> (null_ok(y));
// 创建一个与原有对象 y 的引用计数相同的的智能指针x,borrowed()表示对象是“借用”的,保持引用计数一致
handle<> x(borrowed(y));
handle<> x(null_ok(borrowed(y)));
handle<> x = handle<> (borrowed(y));
handle<> x = handle<> (null_ok(borrowed(y)));
null_ok 和 borrowed 的位置可以互换。
exec 详解
对于大部分的软件工程师来说,了解 exec() 等函数接口就足够了,这部分内容没必要了解。但是学习其具体实现有助于理解 Boost.Python 的实现逻辑,也有助于提升自己的编程能力,所以在这里分析一下。
object BOOST_PYTHON_DECL exec(str string, object global, object local)
{
return exec(python::extract<char const *>(string), global, local);
}
object BOOST_PYTHON_DECL exec(char const *string, object global, object local)
{
// Set suitable default values for global and local dicts.
if (global.is_none())
{
if (PyObject *g = PyEval_GetGlobals())
global = object(detail::borrowed_reference(g));
else
global = dict();
}
if (local.is_none()) local = global;
// should be 'char const *' but older python versions don't use 'const' yet.
char *s = const_cast<char *>(string);
PyObject* result = PyRun_String(s, Py_file_input, global.ptr(), local.ptr());
if (!result) throw_error_already_set();
return object(detail::new_reference(result));
}
在没有指定全局和局部变量的情况下,Boost.Python 默认会借助 PyEval_GetGlobals() 来获取当前执行帧中全局变量的字典。如果失败则定义一个新的字典变量。但是头文件中将 global 和 local 设置了默认参数 dict(),所以一般情况下不会运行到 PyEval_GetGlobals()。
调用 Python 的过程也是借助了 PyRun_String() 方法,只是在此基础上增加了异常处理。
eval() 和 exec_statement() 的实现与 exec() 一样,只是在调用 PyRun_String() 的参数 Py_file_input 不同。
exec_file() 的实现由稍许不同,在此不做分析。
Python C API
Python/C API 是 Python 官方提供的使用 C 或 C++ 扩展 Python 的方法。这种方法需要以特定的方式来编写 C 代码以供 Python 去调用它。所有的 Python 对象都被表示为一种叫做 PyObject 的结构体,并且 Python.h 头文件中提供了各种操作它的函数。大部分对 Python 原生对象的基础函数和操作在 Python.h 头文件中都能找到。
详细教程见:Python/C API Reference Manualhttps://docs.python.org/3/c-api/index.html
完整示例
以下内容来自于 Boost 源代码,从文章完整性的角度供参考,其中有一些暂时还没有看到的内容,后面会补充文章进行说明。
// Copyright Stefan Seefeld 2005.
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
#include <boost/python.hpp>
#include <boost/detail/lightweight_test.hpp>
#include <iostream>
namespace python = boost::python;
// An abstract base class
class Base : public boost::noncopyable
{
public:
virtual ~Base() {};
virtual std::string hello() = 0;
};
// C++ derived class
class CppDerived : public Base
{
public:
virtual ~CppDerived() {}
virtual std::string hello() { return "Hello from C++!";}
};
// Familiar Boost.Python wrapper class for Base
struct BaseWrap : Base, python::wrapper<Base>
{
virtual std::string hello()
{
#if BOOST_WORKAROUND(BOOST_MSVC, <= 1300)
// workaround for VC++ 6.x or 7.0, see
// http://boost.org/libs/python/doc/tutorial/doc/html/python/exposing.html#python.class_virtual_functions
return python::call<std::string>(this->get_override("hello").ptr());
#else
return this->get_override("hello")();
#endif
}
};
// Pack the Base class wrapper into a module
BOOST_PYTHON_MODULE(embedded_hello)
{
python::class_<BaseWrap, boost::noncopyable> base("Base");
}
void exec_test()
{
std::cout << "registering extension module embedded_hello..." << std::endl;
// Register the module with the interpreter
if (PyImport_AppendInittab("embedded_hello", initembedded_hello) == -1)
throw std::runtime_error("Failed to add embedded_hello to the interpreter's "
"builtin modules");
std::cout << "defining Python class derived from Base..." << std::endl;
// Retrieve the main module
python::object main = python::import("__main__");
// Retrieve the main module's namespace
python::object global(main.attr("__dict__"));
// Define the derived class in Python.
python::object result = python::exec(
"from embedded_hello import * \n"
"class PythonDerived(Base): \n"
" def hello(self): \n"
" return 'Hello from Python!' \n",
global, global);
python::object PythonDerived = global["PythonDerived"];
// Creating and using instances of the C++ class is as easy as always.
CppDerived cpp;
BOOST_TEST(cpp.hello() == "Hello from C++!");
std::cout << "testing derived class from C++..." << std::endl;
// But now creating and using instances of the Python class is almost
// as easy!
python::object py_base = PythonDerived();
Base& py = python::extract<Base&>(py_base) BOOST_EXTRACT_WORKAROUND;
// Make sure the right 'hello' method is called.
BOOST_TEST(py.hello() == "Hello from Python!");
std::cout << "success!" << std::endl;
}
void exec_file_test(std::string const &script)
{
std::cout << "running file " << script << "..." << std::endl;
// Run a python script in an empty environment.
python::dict global;
python::object result = python::exec_file(script.c_str(), global, global);
// Extract an object the script stored in the global dictionary.
BOOST_TEST(python::extract<int>(global["number"]) == 42);
std::cout << "success!" << std::endl;
}
void exec_test_error()
{
std::cout << "intentionally causing a python exception..." << std::endl;
// Execute a statement that raises a python exception.
python::dict global;
python::object result = python::exec("print unknown \n", global, global);
std::cout << "Oops! This statement should be skipped due to an exception" << std::endl;
}
int main(int argc, char **argv)
{
BOOST_TEST(argc == 2);
std::string script = argv[1];
// Initialize the interpreter
Py_Initialize();
bool error_expected = false;
if (
python::handle_exception(exec_test)
|| python::handle_exception(boost::bind(exec_file_test, script))
|| (
(error_expected = true)
&& python::handle_exception(exec_test_error)
)
)
{
if (PyErr_Occurred())
{
if (!error_expected)
BOOST_ERROR("Python Error detected");
PyErr_Print();
}
else
{
BOOST_ERROR("A C++ exception was thrown for which "
"there was no exception translator registered.");
}
}
// Boost.Python doesn't support Py_Finalize yet, so don't call it!
return boost::report_errors();
}
参考链接
boost.python/EmbeddingPython - Python Wiki
Embedding - 1.79.0
boost/python/errors.hpp - 1.79.0
boost/python/handle.hpp - 1.79.0
boost.python/handle - Python Wiki
|