简而言之,将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 提供兼容的接口。
从A 的实例构造B
由于 A 和 B 既不是现有的 Python 类型,也不需要鸭子类型,因此不需要自定义转换器。对于B 获取A 的实例,它可以像指定init 获取A 一样简单。
这是A 和B 的简化示例,其中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
但是,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() 生成包装器,但如果对象提供了一个返回int 的get_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::handle 和boost::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 )
;
}
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() 存在,因此当D 传递给B 的构造函数时,A 是从D 的实例构造的。但是,E::get_num() 不存在,并且在尝试从 E 的实例构造 A 时引发异常。
另一种转换解决方案。
对于较大的类型,通过 C-API 实现鸭子类型可能会变得非常复杂。另一种解决方案是在 python 中执行鸭子类型,并将 python 文件与库一起分发。
example_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 的实例将传递给B,因此A_from_python::construct 将不会被调用。因此,输出中缺少打印语句。
虽然这种方法避免了 C-API,从而更容易执行鸭式输入,但它确实有一个主要的权衡,即它需要专门为转换的部分 API 打补丁。另一方面,当自动类型转换功能可用时,无需打补丁。
此外,无论如何,C++ 和 Python 中的访问控制旨在防止意外误用。两者都不能防止故意获取对具有私人可见性的成员的访问权限。在 Python 中执行起来要容易得多,但在 C++ 标准中通过显式模板实例化特别允许这样做。