如何使用Boost.Python将C ++对象传递给另一个C ++对象

时间:2012-08-12 22:25:42

标签: c++ python boost boost-python

我有一些C ++代码定义了两个类A和B. B在构造期间采用A的实例。我用Boost.Python包装了A,以便Python可以创建A的实例以及子类。我想对B做同样的事。

class A {
    public:
        A(long n, long x, long y) : _n(n), _x(x), _y(y) {};
        long get_n() { return _n; }
        long get_x() { return _x; }
        long get_y() { return _y; }
    private:
        long _n, _x, _y;
};

class B {
    public:
        B(A a) : _a(a) {};
        doSomething() { ... };
    private:
        A _a;
};

在包装B时,我需要弄清楚如何将A的实例传递给B的构造函数。我做了一些挖掘,我发现的solution是写一个“转换器”类:

struct A_from_python_A {
    static void * convertible(PyObject* obj_ptr) {
        // assume it is, for now...
        return obj_ptr;
    }

    // Convert obj_ptr into an A instance
    static void construct(PyObject* obj_ptr,
                      boost::python::converter::rvalue_from_python_stage1_data* data) {
        // extract 'n':
        PyObject * n_ptr = PyObject_CallMethod(obj_ptr, (char*)"get_n", (char*)"()");
        long n_val = 0;
        if (n_ptr == NULL) {
            cout << "... an exception occurred (get_n) ..." << endl;
        } else {
            n_val = PyInt_AsLong(n_ptr);
            Py_DECREF(n_ptr);
        }

        // [snip] - also do the same for x, y

        // Grab pointer to memory into which to construct the new A
        void* storage = (
            (boost::python::converter::rvalue_from_python_storage<A>*)
            data)->storage.bytes;

        // in-place construct the new A using the data
        // extracted from the python object
        new (storage) A(n_val, x_val, y_val);

        // Stash the memory chunk pointer for later use by boost.python
        data->convertible = storage;
    }

    // register converter functions
    A_from_python_A() {
        boost::python::converter::registry::push_back(
            &convertible,
            &construct,
            boost::python::type_id<A>());
    }
};

然后我注册:

BOOST_PYTHON_MODULE(interpolation_ext)
{
    // register the from-python converter for A
    A_from_python_A();

    class_<A>("A", init<long, long, long>())
        ;

    class_<B>("B", init<object>())
        ;
}

Convertible和construct是回答“这是可兑换的吗?”的方法。和“如何转换?”问题分别。我观察到construct()方法是非平凡的 - 它必须到达A的PyObject *,提取所有相关字段,然后重建一个C ++实例,然后传递给B的构造函数。因为A包含一些私有字段,所以它必须通过公共访问机制来做到这一点(而对于纯Python对象,它不必,对吧?)。这似乎有效。

然而,是'构造'函数中的字段提取真的有必要吗?这似乎很费力。如果A是复合对象,它可能变得非常复杂,并且可能需要一个转换器来调用另一个转换器。我可能理解A是一个Python类的要求,但如果A实例来自C ++端,是否有办法确定是这种情况,然后只需获得一个句柄(例如指针)到这个'native'对象,作为捷径?

这是相关的python代码:

from my_ext import A, B
a = A(1,2,3)
b = B(a)
b.doSomething()

1 个答案:

答案 0 :(得分:8)

简而言之,将B的包装器定义为:

class_<B>( "B", init< A >() )

而不是

class_<B>( "B", init< object >() )

在Boost.Python中定义类的包装器时(至少在1.50中),class_模板生成转换和构造函数。这允许A转换为A的包装并从中构造。这些PyObject转换具有严格的类型检查,并且要求在python中使用以下内容:isinstance( obj, A )

自定义转换器通常用于支持:

  • 与现有Python类型之间的自动转换。例如,将std::pair< long, long >转换为PyTupleObject
  • 鸭打字。例如,只要B提供兼容的界面,D接受A类就不会从D派生。

B

的实例构建A

由于AB既不是现有的Python类型,也不需要鸭子类型,因此不需要自定义转换器。要让B获取A的实例,可以像指定init获取A一样简单。

以下是AB的简化示例,其中B可以从A构建。

class A
{
public:
  A( long n ) : n_( n ) {};
  long n() { return n_; }
private:
  long n_;
};

class B
{
public:
  B( A a ) : a_( a ) {};
  long doSomething() { return a_.n() * 2; }
private:
  A a_;
};

包装器将被定义为:

using namespace boost::python;
BOOST_PYTHON_MODULE(example)
{
  class_< A >( "A", init< long >() )
    ;

  class_<B>( "B", init< A >() )
    .def( "doSomething", &B::doSomething )
    ;
}

B的包装器明确指出它将通过Ainit< A >()对象构造。此外,A的接口未完全暴露给Python对象,因为没有为A::n()函数定义包装器。

>>> from example import A, B
>>> a = A( 1 )
>>> b = B( a )
>>> b.doSomething()
2

这也适用于派生自A的类型。例如:

>>> from example import A, B
>>> class C( A ):
...     def __init__( self, n ):
...         A.__init__( self, n )
... 
>>> c = C( 2 )
>>> b = B( c )
>>> b.doSomething()
4

但是,未启用鸭子输入。

>>> from example import A, B
>>> class E: pass
... 
>>> e = E()
>>> b = B( e )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    B.__init__(B, instance)
did not match C++ signature:
    __init__(_object*, A)

从可转换为B的对象构造A

为了支持可以从提供兼容接口的对象构造B的情况,需要自定义转换器。虽然以前没有为A::n()生成包装器,但是如果对象提供返回A的{​​{1}}方法,则继续声明对象可以转换为get_num()

首先,编写一个提供转换器和构造函数的int结构。

A_from_python
如果返回struct A_from_python { static void* convertible( PyObject* obj_ptr ) { // assume it is, for now... return obj_ptr; } // Convert obj_ptr into an A instance static void construct( PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data) { std::cout << "constructing A from "; PyObject_Print( obj_ptr, stdout, 0 ); std::cout << std::endl; // Obtain a handle to the 'get_num' method on the python object. // If it does not exists, then throw. PyObject* n_ptr = boost::python::expect_non_null( PyObject_CallMethod( obj_ptr, (char*)"get_num", (char*)"()" )); long n_val = 0; n_val = PyInt_AsLong( n_ptr ); Py_DECREF( n_ptr ); // Grab pointer to memory into which to construct the new A void* storage = ( (boost::python::converter::rvalue_from_python_storage< A >*) data)->storage.bytes; // in-place construct the new A using the data // extracted from the python object new ( storage ) A( n_val ); // Stash the memory chunk pointer for later use by boost.python data->convertible = storage; } A_from_python() { boost::python::converter::registry::push_back( &convertible, &construct, boost::python::type_id< A >() ); } };

boost::python::expect_non_null用于抛出异常。这有助于提供python对象必须提供NULL方法的鸭子类型保证。如果已知get_num是给定类型的实例,则可以使用boost::python::api::handleboost::python::api::object直接提取类型,并避免必须通过{一般性地进行调用{1}}界面。

接下来,将转换器注册到模块中。

PyObject

PyObjectusing namespace boost::python; BOOST_PYTHON_MODULE(example) { // register the from-python converter for A A_from_python(); class_< A >( "A", init< long >() ) ; class_<B>( "B", init< A >() ) .def( "doSomething", &B::doSomething ) ; } 或其关联的包装器定义未发生任何更改。创建了自动转换功能,然后在模块中定义/注册。

A

B存在,因此当>>> from example import A, B >>> a = A( 4 ) >>> b = B( a ) >>> b.doSomething() 8 >>> class D: ... def __init__( self, n ): ... self.n = n ... def get_num( self ): ... return self.n ... >>> d = D( 5 ) >>> b = B( d ) constructing A from <__main__.D instance at 0xb7f7340c> >>> b.doSomething() 10 >>> class E: pass ... >>> e = E() >>> b = B( e ) constructing A from <__main__.E instance at 0xb7f7520c> Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: get_num 传递给D::get_num()的构造函数时,A是从D的实例构造的。但是,D不存在,尝试从B的实例构建E::get_num()时会引发异常。


另一种转换解决方案。

使用更大类型,通过C-API实现duck-typing会变得非常复杂。另一种解决方案是在python中执行duck-typing,并将python文件与库一起分发。

A将导入Eexample_ext.py类型,以及猴子补丁A的构造函数:

B

最终用户所需的唯一更改是导入B而不是from example import A, B def monkey_patch_B(): # Store handle to original init provided by Boost. original_init = B.__init__ # Construct an A object via duck-typing. def construct_A( obj ): return A( obj.get_num() ) # Create a new init that will delegate to the original init. def new_init( self, obj ): # If obj is an instance of A, use it. Otherwise, construct # an instance of A from object. a = obj if isinstance( obj, A ) else construct_A ( obj ) # Delegate to the original init. return original_init( self, a ) # Rebind the new_init. B.__init__ = new_init monkey_patch_B()

example_ext

由于修补的构造函数保证将example的实例传递给>>> from example_ext import A, B >>> a = A( 6 ) >>> b = B( a ) >>> b.doSomething() 12 >>> class D: ... def __init__( self, n ): ... self.n = n ... def get_num( self ): ... return self.n ... >>> d = D( 7 ) >>> b = B( d ) >>> b.doSomething() 14 >>> class E: pass ... >>> e = E() >>> b = B( e ) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "example_ext.py", line 15, in new_init a = obj if isinstance( obj, A ) else construct_A ( obj ) File "example_ext.py", line 9, in construct_A return A( obj.get_num() ) AttributeError: E instance has no attribute 'get_num' ,因此不会调用A。因此,输出中缺少打印语句。

虽然这种方法避免使用C-API,因此更容易执行鸭子类型,但它确实有一个主要的权衡,因为它需要对API的某些部分进行特殊修补以进行转换。另一方面,当自动类型转换功能可用时,不需要修补。


此外,对于它的价值,C ++和Python中的访问控制旨在防止意外滥用。既不能防止故意获取具有私人可见性的成员的访问权限。在Python中更容易做到,但在C ++标准中通过显式模板实例化特别允许它。