这篇笔记用于详细说明如何将c++中的类转换成在python环境可以直接使用的类。
这里定义了一个简单的c++类RealWorld,包含public,private成员和public成员函数。在这个示例中会展示如何将类的成员函数以及成员变量转换成python内的对象。
代码构成如下,classes.hpp/cpp包含类的定义和实现,classes.py为Python测试文件,CMakeLists.txt是构建文件。
02_ExposingClass$ tree ├── classes.cpp ├── classes.hpp ├── classes.py └── CMakeLists.txt
#include <string> class RealWorld { public: RealWorld(std::string n, char sex) : name(n), sex('m'), age(0.0){}; std::string name; void Welcome(); void SetAge(int age); int GetAge(); std::string GetName(); char GetSex(); private: char sex; int age; };
#include <iostream> #include <boost/python.hpp> #include "classes.hpp" namespace python = boost::python; void RealWorld::Welcome() { std::cout << "Welcome to real world" << std::endl; } int RealWorld::GetAge() { return age; } void RealWorld::SetAge(int value) { age = value; } std::string RealWorld::GetName() { return name; } char RealWorld::GetSex() { return sex; } // 转换成classes module BOOST_PYTHON_MODULE(classes) { python::class_<RealWorld> ("RealWorld", python::init<std::string, char>()) // Expose functions .def ("Welcome", &RealWorld::Welcome) .def ("GetAge", &RealWorld::GetAge) .def ("SetAge", &RealWorld::SetAge, python::args("value")) .def ("GetName", &RealWorld::GetName) .def ("GetSex", &RealWorld::GetSex) // Expose member .def_readwrite("name", &RealWorld::name) .add_property("age", &RealWorld::GetAge, &RealWorld::SetAge) .add_property("sex", &RealWorld::GetSex) ; }
c++类中的public成员变量,对应在python里面是一个可读可写的
set(MODULE_NAME classes) include_directories(${CMAKE_SOURCE_DIR}) add_library(${MODULE_NAME} SHARED classes.cpp ) if (UNIX) set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "" ) elseif (WIN32) set_target_properties(${MODULE_NAME} PROPERTIES SUFFIX ".pyd" ) endif() target_link_libraries(${MODULE_NAME} ${Boost_LIBRARIES} ${PYTHON_LIBRARIES} )
#!/usr/bin/env python import classes t1 = classes.RealWorld("Xiangdi", 'm') t1.Welcome() t1.SetAge(20) print (t1.name, "'s age is ", t1.age, "sex is ", t1.sex) t1.name = "Xiaoming" # t1.sex = 'f' # sex has no set function, so can't be setted t1.age = 25 print (t1.name, "'s age is ", t1.age, "sex is ", t1.sex)
在上一级的CMakeLists.txt文件中包含当前目录
ADD_SUBDIRECTORY(02_ExposingClass)
编译
cd boost cmake .. make
运行
cd build/lib cp ../../02_ExposingClass/classes.py . python classes.py Welcome to real world Xiangdi 's age is 20 sex is m Xiaoming 's age is 25 sex is m
通常有两种方法将c++类转换成python的object
class A { ... }; BOOST_PYTHON_MODULE(example) { class_<A>("A"); }
另外一种则是直接在python module里创建c++类的实例
BOOST_PYTHON_MODULE(example1) { object class_a = class_<A>("A"); object instance_a = class_a(); }
Boost.Python会尝试注册一个转换器来处理wrapped函数,这些函数处理class类型的函数返回值,也就是说默认情况下必须能够将C++类构造的实例复制到可由python管理的对象存储中。而对于抽象类,本身是不会实例化的,那么就需要用户告诉Boost.Python这个类是不能复制的。
class_< Abstract , boost::noncopyable>("Abstract", no_init);
这里的Abstract对应的是C++类名称,通过no_init
关键字来声明这个类无法复制。
Boost.Python 允许您指定 Python 对象将如何保存它们包装的 C++ 对象。您可以指定它们由 shared_ptr< T >
(或任何其他智能指针)保存,在这种情况下,库将为 shared_ptr< T >
生成到/从 Python 的转换器。 to_python 转换器将简单地围绕 shared_ptr< >
构建一个新的 Python 对象。您可以指定您的 C++ 对象由 shared_ptr< U >
持有。这允许您持有一个用于调度的 U 对象,但仍然在您的 C++ 代码中传递 shared_ptrs。
如果你有想要在 Python 中覆盖的虚函数,你实际上必须使用派生类 U 来保存 T 对象,它覆盖了虚函数以分派回 Python。在这种情况下,类 U 自然必须有权访问 Python 对象
上述安排有几个问题,但最重要的一个问题是,如果让 shared_ptr< U >
比其对应的 Python 对象存活时间更长,则对 Python 可覆盖的虚函数的调用将崩溃,因为它们会尝试调用通过无效的指针。
class_<A>("A") .def(init<int, int>()) .def(...) ; class_<B>("B", init<int, int>()) .def(...) ; class_<C>("C", "C's docstring", init<int, int>()) .def(...) ; class_<D>("D", "D's docstring", init<int, int>(), "__init__ doc") .def(...) ; class_<E>("E") .def(...) ; class_<F>("F", no_init) .def(...) ; class_<G>("G", "G's docstring", no_init) .def(...) ; class_<H>("H", "H's docstring") .def(...) ;
class_是一个模板类,定义在boost/python/class.hpp文件中
// This is the primary mechanism through which users will expose // C++ classes to Python. template < class W // class being wrapped , class X1 // = detail::not_specified , class X2 // = detail::not_specified , class X3 // = detail::not_specified > class class_ : public objects::class_base { public: // types typedef objects::class_base base; typedef class_<W,X1,X2,X3> self; typedef typename objects::class_metadata<W,X1,X2,X3> metadata; typedef W wrapped_type; ... }
创建与作为其第一个参数传递的 C++ 类型关联的 Python 类。虽然它有四个模板参数,但只有第一个是必需的(即W),它代表要封装的c++类。后面三个参数是可选的(X1/X2/X3),实际上可以按任何顺序提供; Boost.Python 根据参数的类型确定参数的角色。
需要注意的是,X1/X2/X3一定下列类型的参数:
参数 | 说明 |
---|---|
Base | bases<…> 的一种特化,它指定了 W 的先前公开的 C++ 基类。 |
HeldType | 必须是 W、从 W 派生的类、或指针::type 为 W 的可解引用类型或从 W 派生的类。 指定在调用 T 的构造函数时或在不使用 ptr、ref 或调用策略 |
NonCopyable | 禁止自动注册复制 W 实例的 to_python 转换。当 W 没有可公开访问的复制构造函数时需要。如果有,必须是boost::noncopyable |
// Constructors with default __init__ class_(char const* name); class_(char const* name, char const* docstring); // Constructors, specifying non-default __init__ template <class Init> class_(char const* name, Init); template <class Init> class_(char const* name, char const* docstring, Init);
可以看到class_共提供多种构造函数,除了name是必不可少的之外,其他都是可以缺省的。留给用户自由发挥的空间还是很大的。需要注意的是,如果在class_实例化时没有显式标识"no_init",并不代表类没有构造函数或init()
,只是构造函数不需要参数。所以对于无构造函数的类(如抽象类),是需要显式标识"no_init"的。
实际上除了官网给出的列表,还有其他的构造方法可供调用,具体可见boost源代码。
class_的成员函数通过def()函数来封装c++类中的成员函数或非成员函数,同样也提供了多种重载类型可以选择。
// Exposing additional __init__ functions template <class Init> class_& def(Init); // defining methods template <class F> class_& def(char const* name, F f); template <class Fn, class A1> class_& def(char const* name, Fn fn, A1 const&); template <class Fn, class A1, class A2> class_& def(char const* name, Fn fn, A1 const&, A2 const&); template <class Fn, class A1, class A2, class A3> class_& def(char const* name, Fn fn, A1 const&, A2 const&, A3 const&);
class_& def(Init);是固定的使用方式,这样可以独立封装类的构造函数。
name表示python封装后的函数名称,F/Fn是对应的c++函数名称,A1/A2/A3表示属性,可以分别对应docstring/policies/keywords,这三者可以以任何数量和顺序出现。在Boost.Python中,包括参数args(),返回值类型return_value_policy()等都是以类/obj的形式出现的。
A1-A3分别对应下表的内容:
名称 | 属性 | 说明 |
---|---|---|
docstring | Any ntbs | 值将被绑定到python方法的__doc__属性中 |
policies | CallPolicies模型 | 函数结果的封装策略 |
keywords | 参数 | 用于表示函数参数 |
这里的成员变量对应于c++类中的成员变量,是可以直接在外部访问的内容,根据属性的不同可以封装成只读/可读可写两种类型。
// exposing data members template <class D> class_& def_readonly(char const* name, D T::*pm); template <class D> class_& def_readwrite(char const* name, D T::*pm); // exposing static data members template <class D> class_& def_readonly(char const* name, D const& d); template <class D> class_& def_readwrite(char const* name, D& d);
这里的属性对应于c++类中的私有或保护变量,这类变量在外部不能直接访问,只能通过类内的访问接口。
self& add_property(char const* name, Get fget, char const* docstr = 0) self& add_property(char const* name, Get fget, Set fset, char const* docstr = 0) // 添加静态成员变量 self& add_static_property(char const* name, Get fget) self& add_static_property(char const* name, Get fget, Set fset)
在添加私有变量时,后面依次跟着读取和设置方法,注意需要至少提供读取接口(Get)。
创建一个新的Python属性类实例,将带有(可选的)文档字符串doc的object(fget)(以及第二种形式的object(fset))传递给其构造函数,然后将该属性添加到带有给定属性名的正在构造的Python类对象中。
class_& staticmethod(char const* name);
将name指定的函数声明成python下的静态函数,相当于python语句:
setattr(self, name, staticmethod(getattr(self, name)))
这里只显示了部分class_提供的方法,对于大部分场景已经够用了(我猜的),还有一些其他的接口,在后面需要用到的时候再详细分析一下,这里就不瞎猜了。
class_<> statement constructs python class object.
boost/python/class.hpp