【问题标题】:How to pass a C++ object to another C++ object with Boost.Python如何使用 Boost.Python 将 C++ 对象传递给另一个 C++ 对象
【发布时间】:2012-08-12 22:25:42
【问题描述】:

我有一些 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 对象,它不必这样做,对吗?)。这似乎有效。

但是,'construct' 函数中的字段提取真的有必要吗? 看起来很费力。如果 A 是一个复合对象,它可能会变得非常复杂,并且可能需要一个转换器来调用另一个转换器。如果 A 是 Python 类,我可能理解要求,但是如果 A 实例源自 C++ 端,有没有办法确定是这种情况,然后简单地获取这个“本机”的句柄(例如指针)对象,作为快捷方式?

这是相关的python代码:

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

【问题讨论】:

    标签: c++ python boost boost-python


    【解决方案1】:

    简而言之,将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&lt; long, long &gt;PyTupleObject 相互转换。
    • 鸭式打字。例如,让B 接受类D,它不是从A 派生的,只要D 提供兼容的接口。

    A 的实例构造B

    由于 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 的包装器明确指出它将通过init&lt; A &gt;()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
    

    但是,duck-typing 没有启用。

    >>> 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 作为参数的 /non-constructor/ 方法?例如。如果我有“class B { public: long doSomethingElse(A aa) { return aa.n() * 3; } };”那么在这种情况下,Boost.Python 是否也会自动处理与 A 的转换(根据您的第一个完整示例)?
    • 是的。当函数将A 作为参数时,Python 将尝试从传递给函数的PyObject 构造A C++ 类型。如果 Python 对象是A Python 包装器的一个实例,则A C++ 类型是从A Python 包装器中提取的,并且很可能被复制到C++ 函数参数中。如果提取失败,那么 Boost.Python 将尝试使用注册到 boost::python::type_id&lt; A &gt; 的构造函数构造一个 A Python 包装器。
    猜你喜欢
    • 2015-09-15
    • 1970-01-01
    • 1970-01-01
    • 2012-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-23
    • 1970-01-01
    相关资源
    最近更新 更多