Boost(3):将c++类封装成python类

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

上一篇:boost::bind with ros topic,ros中subscribe用boost::bind绑定多个参数


下一篇:Boost C++