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

8

我有一些定义了两个类A和B的C++代码。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的构造函数。我进行了一些探索,找到了这个“转换器”类的解决方案
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对象就不需要,对吧?)。这似乎有效。
然而,“construct”函数中的字段提取真的必要吗?它似乎很费力。如果A是一个复合对象,它可能会变得非常复杂,并且可能需要一个转换器来调用另一个转换器。如果A是一个Python类,那么我或许能理解这种要求,但如果A实例来自C++端,有没有一种方法可以确定这种情况,然后简单地获得一个句柄(例如指针)来访问这个“本地”对象,以便快速进行操作?
以下是相关的Python代码:
from my_ext import A, B
a = A(1,2,3)
b = B(a)
b.doSomething()
1个回答

10

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

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

代替

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

在Boost.Python中定义类的包装器时(至少在1.50版本中),class_模板会生成转换和构造函数。这使得可以将A转换为A的包装器,并从A的包装器构造A。这些PyObject转换具有严格的类型检查,并要求在Python中满足以下条件:isinstance(obj, A)

通常使用自定义转换器来支持以下操作:

  • 自动转换为现有的Python类型。例如,将std::pair<long, long>转换为PyTupleObject
  • 鸭子类型。例如,使B接受不是从A派生的类D,只要D提供兼容的接口即可。

A的实例构造B

由于AB既不是现有的Python类型,也不需要鸭子类型,因此不需要自定义转换器。对于BA的实例中获取值,只需要简单地指定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 的包装器明确指示它将通过 init< A >() 从一个 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)

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

如果要支持将一个提供兼容接口的对象转换为B的情况,则需要使用自定义转换器。尽管以前没有为A::n()生成包装器,但让我们仍然假设如果该对象提供了一个返回intget_num()方法,则该对象可以被转换为A

首先,编写一个名为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用于在返回NULL时抛出异常。这有助于提供鸭子类型保证,即Python对象必须提供get_num方法。如果已知PyObject是给定类型的实例,则可以使用boost::python::api::handleboost::python::api::object直接提取类型,并避免通过PyObject接口通用调用。

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

using 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 )
    ;
}

没有对AB或它们相关的封装定义进行任何更改。自动转换函数是在模块内创建并定义/注册的。

>>> 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() 存在,因此当将 D 传递给 B 的构造函数时,A 是由 D 的实例构建的。然而,E::get_num() 不存在,当试图从 E 的实例构建 A 时会引发异常。


另一种转换方案。

通过 C-API 实现鸭子类型可能变得非常复杂,特别是针对较大的类型。另一种解决方案是在 Python 中执行鸭子类型,并将 Python 文件与库一起分发。

example_ext.py 将导入 AB 类型,以及猴子补丁 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的实例传递给B,因此A_from_python::construct不会被调用。因此输出中缺少了打印语句。

虽然这种方法避免了使用C-API,使得鸭子类型更容易实现,但它有一个主要的折衷方案,即需要特别为转换补丁API的部分。另一方面,当自动类型转换函数可用时,无需进行任何补丁。


此外,无论是C++还是Python中的访问控制都旨在防止意外的误用。两者都不能防止故意获取具有私有可见性的成员。在Python中,这要容易得多,但在C++标准中通过显式模板实例化是明确允许的。


这是否同样适用于以A作为参数的/非构造函数/方法?例如,如果我有“class B { public: long doSomethingElse(A aa) { return aa.n() * 3; } };",那么Boost.Python是否也会自动处理对/从A的转换(根据您的第一个完整示例)? - davidA
1
是的。当一个函数以A作为参数时,Python将尝试从传递给函数的PyObject构造A C++类型。如果Python对象是A Python包装器的实例,则从A Python包装器中提取A C++类型,并可能被复制构造到C++函数参数中。如果提取失败,则Boost.Python将尝试使用注册到boost::python::type_id< A >的构造函数构造A Python包装器。 - Tanner Sansbury

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接