8

AとBの2つのクラスを定義するC++コードがいくつかあります。Bは構築中にAのインスタンスを取ります。PythonがAのインスタンスとサブクラスを作成できるように、AをBoost.Pythonでラップしました。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>())
        ;
}

コンバーチブルとコンストラクトは、「これはコンバーチブルですか?」に答えるメソッドです。と「変換する方法は?」それぞれ質問。私は、construct()メソッドが重要であることを確認しました。AのPyObject *に到達し、関連するすべてのフィールドを抽出してから、C ++インスタンスを再構築して、Bのコンストラクターに渡す必要があります。Aにはいくつかのプライベートフィールドが含まれているため、パブリックアクセスメカニズムを介してこれを行う必要があります(純粋なPythonオブジェクトでは、そうする必要はありませんよね?)。これはうまくいくようです。

しかし、「構築」関数でのフィールド抽出は本当に必要ですか?面倒そうです。Aが複合オブジェクトの場合、非常に複雑になる可能性があり、1つのコンバーターが別のコンバーターを呼び出す必要がある可能性があります。AがPythonクラスである場合の要件はおそらく理解できますが、AインスタンスがC ++側から発生した場合、これが当てはまると判断し、この「ネイティブ」へのハンドル(ポインターなど)を取得する方法はありますか?オブジェクト、ショートカットとして?

関連するPythonコードは次のとおりです。

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

1 に答える 1

10

つまり、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クラスを受け入れることができます。DAD

Bのインスタンスからの構築A

ABは既存の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のラッパーは、Aを介してオブジェクトから構築されることを明示的に示しますinit< 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()変換できるというステートメントを続けましょう。Aget_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_nullNULLが返された場合に例外をスローするために使用されます。これは、Pythonオブジェクトがメソッドを提供する必要があるというダックタイピングの保証を提供するのに役立ちますget_numPyObjectが特定のタイプのインスタンスであることがわかっている場合は、タイプを使用し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()が存在するため、のコンストラクターに渡されるときAのインスタンスから構築されます。ただし、は存在せず、のインスタンスから構築しようとすると例外が発生します。DDBE::get_num()AE


代替の変換ソリューション。

C-APIを介したダックタイピングの実装は、タイプが大きくなると非常に複雑になる可能性があります。別の解決策は、Pythonでダックタイピングを実行し、Pythonファイルをライブラリと一緒に配布することです。

example_ext.pyABタイプ、およびモンキーパッチ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呼び出されません。したがって、出力に欠落しているprintステートメント。

このアプローチはC-APIを回避し、ダックタイピングの実行を容易にしますが、変換のためにAPIの一部に特別なパッチを適用する必要があるという大きなトレードオフがあります。一方、自動型変換機能を使用できる場合は、パッチを適用する必要はありません。


また、C ++とPythonの両方のアクセス制御は、偶発的な誤用から保護することを目的としています。どちらも、私的な可視性を持つメンバーへのアクセスを故意に取得することから保護しません。Pythonで行う方がはるかに簡単ですが、C ++標準では、明示的なテンプレートのインスタンス化によって特に許可されています。

于 2012-08-15T20:43:28.920 に答える