1. 说明
这篇笔记用于详细说明如何将c++中的类转换成在python环境可以直接使用的类。
2. 示例
这里定义了一个简单的c++类RealWorld,包含public,private成员和public成员函数。在这个示例中会展示如何将类的成员函数以及成员变量转换成python内的对象。
2.1 整体代码
代码构成如下,classes.hpp/cpp包含类的定义和实现,classes.py为Python测试文件,CMakeLists.txt是构建文件。
02_ExposingClass$ tree
├── classes.cpp
├── classes.hpp
├── classes.py
└── CMakeLists.txt
2.2 classes.hpp
#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;
};
2.3 classes.cpp
#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里面是一个可读可写的
2.3
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}
)
2.5 classes.py
#!/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)
2.6 编译运行
在上一级的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
3. Expose class to python的方法
通常有两种方法将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(...)
;
- init<int, int>()表示c++类的构造函数的参数,对应于python下类的__init__函数可以在()内设置默认值;
- no_init表示无构造函数,那么对应的python类也就不会有__init__()
- 其他的"doc/docstring"表示描述信息
4. class_类详解
class_是一个模板类,定义在boost/python/class.hpp文件中
4.1 class_定义
// 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 |
4.2 构造函数
// 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源代码。
4.2 封装成员换函数
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 | 参数 | 用于表示函数参数 |
4.3 封装成员变量
这里的成员变量对应于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);
- name:封装后在python类内的变量名称;
- d:对应要封装的c++的变量
4.4 创建属性
这里的属性对应于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类对象中。
4.5 声明静态函数
class_& staticmethod(char const* name);
将name指定的函数声明成python下的静态函数,相当于python语句:
setattr(self, name, staticmethod(getattr(self, name)))
5. 总结
这里只显示了部分class_提供的方法,对于大部分场景已经够用了(我猜的),还有一些其他的接口,在后面需要用到的时候再详细分析一下,这里就不瞎猜了。
参考资料
class_<> statement constructs python class object.
boost/python/class.hpp