チュートリアル

Joel de Guzman
David Abrahams
© 2002-2005 Joel de Guzman, David Abrahams
Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt

クイックスタート

Boost.Python ライブラリは Python と C++ 間のインターフェイスのためのフレームワークである。C++ のクラス、関数、オブジェクトをすばやくシームレスに Python へエクスポートでき、また逆に Python から C++ へエクスポートできる。特別なツールは必要なく、あなたのコンパイラだけで可能だ。このライブラリは C++ インターフェイスを非侵入的にラップするよう設計されており、C++ コードを変更する必要は一切ない。このため、サードパーティ製ライブラリを Python へエクスポートするには Boost.Python が最適である。ライブラリは高度なメタプログラミング技術を使ってその構文を単純化しており、コードのラッピングは宣言的なインターフェイス定義言語(IDL)のような見た目になっている。

Hello World

C/C++ の伝統に従い「hello, world」から始めるとしよう。

char const* greet()
{
   return "hello, world";
}

この C++ 関数は、次の Boost.Python ラッパを書くことで Python へエクスポートできる。

#include <boost/python.hpp>

BOOST_PYTHON_MODULE(hello_ext)
{
    using namespace boost::python;
    def("greet", greet);
}

これですべてである。あとはこれを共有ライブラリとしてビルドすると、生成した DLL が Python から可視となる。以下は Python のセッション例である。

>>> import hello_ext
>>> print hello_ext.greet()
hello, world

Hello World のビルド

始めから終わりまで

まず最初は Hello World モジュールをビルドして Python で使ってみることだろう。本節でそのためのステップを明らかにする。あらゆる Boost ディストリビューションに付属するビルドツールである bjam を使用する。

注釈

bjam を使用せずにビルドする

当然 bjam 以外にモジュールをビルドする方法はある。ここに書いていることが「唯一の方法」というわけではない。bjam の他にビルドツールは存在する。

しかしながら Boost.Python のビルドには bjam が適していると記しておく。セットアップを失敗させる方法はたくさんある。経験から言えば「Boost.Python がビルドできない」という問題の 9 割は、他のツールを使用することを余儀なくされた人から寄せられた。

細かいことは省略する。ここでの目的は Hello World モジュールを簡単に作成して Python で走らせることである。Boost.Python のビルドについて完全なリファレンスは「ビルドとテスト」を見るとよい。この短いチュートリアルが終わったら DLL のビルドが完了して Python のプログラムで拡張が走っているはずである。

チュートリアルの例はディレクトリ /libs/python/example/tutorial にある。以下のファイルがある。

  • hello.cpp

  • hello.py

  • Jamroot

hello.cpp ファイルは C++ の Hello World 例、Jamroot は DLL をビルドする最小限の bjam スクリプトである。そして hello.pyhello.cpp の拡張を使用する Python プログラムである。

何よりもまず bjam の実行可能ファイルを boost ディレクトリか、bjam をコマンドラインから実行できるパスに置いておく。ほとんどのプラットフォームでビルド済み Boost.Jam 実行可能ファイルが利用できる。bjam 実行可能ファイルの完全なリストがここにある。

Jam ろう!

最小限の Jamroot ファイルを /libs/python/example/tutorial/Jamroot に置いておく。そのままファイルをコピーして use-project boost の部分を Boost のルートディレクトリに設定すればよい。

必要なことはこの Jamroot ファイルのコメントに書いてある。

bjam を起動する

オペレーティングシステムのコマンドラインインタープリタから bjam を起動する。

では、始めるとしよう。

user-config.jam という名前のファイルをホームディレクトリに置いてツールを調整する。Windows の場合、コマンドプロンプトウィンドウで次のようにタイプするとホームディレクトリがわかる。

ECHO %HOMEDRIVE%%HOMEPATH%

ファイルには少なくともコンパイラと Python のインストールについてのルールを書いておく必要がある。Windows の場合は例えば以下のとおり:

#  Microsoft Visual C++ の設定
using msvc : 8.0 ;

#  Python の設定
using python : 2.4 : C:/dev/tools/Python ;

1 番目のルールで MSVC 8.0 とその関連ツールを使用することを bjam に設定している。2 番目のルールは Python についての設定であり、Python のバージョンと場所を指定している。上の例では C:/dev/tools/Python に Python をインストールした想定である。Python を正しく「標準的に」インストールした場合はこの設定は不要である。

ここまで来れば準備は整った。チュートリアルの hello.cppJamroot が置いてある libs/python/example/tutorialcd で移動するのを忘れないように。

bjam

これでビルドが始まり、

cd C:\dev\boost\libs\python\example\tutorial
bjam
...patience...
...found 1101 targets...
...updating 35 targets...

最終的に例えば以下のように表示される。

Creating library path-to-boost_python.dll
Creating library /path-to-hello_ext.exp/
**passed** ... hello.test
...updated 35 targets...

すべて問題なければ、DLL がビルドされ Python のプログラムが走るはずである。

さあ、楽しんでいただきたい!

クラスのエクスポート

では C++ クラスを Python へエクスポートしよう。

エクスポートすべき C++ クラス・構造体を考えよう。

struct World
{
    void set(std::string msg) { this->msg = msg; }
    std::string greet() { return msg; }
    std::string msg;
};

相当する Boost.Python ラッパを書いて Python へエクスポートできる。

#include <boost/python.hpp>
using namespace boost::python;

BOOST_PYTHON_MODULE(hello)
{
    class_<World>("World")
        .def("greet", &World::greet)
        .def("set", &World::set)
    ;
}

上記のようにメンバ関数 greet および set をエクスポートする C++ クラスラッパを書いた。このモジュールを共有ライブラリとしてビルドすると、Python 側から World クラスが使用できるようになる。次に示すのは Python のセッション例である。

>>> import hello
>>> planet = hello.World()
>>> planet.set('howdy')
>>> planet.greet()
'howdy'

コンストラクタ

前回の例では明示的なコンストラクタが登場しなかった。World はプレーンな構造体として宣言したので、暗黙のデフォルトコンストラクタとなっていた。Boost.Python は既定ではデフォルトコンストラクタをエクスポートするので、以下のように書けた。

>>> planet = hello.World()

デフォルトでないコンストラクタを使ってクラスをラップしたい場合もあるだろう。前回の例をビルドする。

struct World
{
    World(std::string msg): msg(msg) {} // コンストラクタを追加した
    void set(std::string msg) { this->msg = msg; }
    std::string greet() { return msg; }
    std::string msg;
}

これで World にデフォルトコンストラクタはなくなった。前回のラップコードは、ライブラリをエクスポートするところでコンパイルに失敗するだろう。代わりにエクスポートしたいコンストラクタについて class_<World> に通知しなければならない。

#include <boost/python.hpp>
using namespace boost::python;

BOOST_PYTHON_MODULE(hello)
{
    class_<World>("World", init<std::string>())
        .def("greet", &World::greet)
        .def("set", &World::set)
    ;
}

init<std::string> が、std::string を引数にとるコンストラクタをエクスポートする(Python ではコンストラクタを「"__init__"」と書く)。

def メンバ関数に init<...> を渡すことでエクスポートするコンストラクタを追加できる。例えば Worlddouble を 2 つとる別のコンストラクタがあるとすれば、

class_<World>("World", init<std::string>())
    .def(init<double, double>())
    .def("greet", &World::greet)
    .def("set", &World::set)
;

逆にコンストラクタを 1 つもエクスポートしたくない場合は、代わりに no_init を使う。

class_<Abstract>("Abstract", no_init)

これは実際には、常に Python の RuntimeError 例外を投げる __init__ メソッドを追加する。

クラスデータメンバ

データメンバもまた Python へエクスポートでき、対応する Python クラスの属性としてアクセス可能になる。各データメンバは読み取り専用読み書き可能として見なすことができる。以下の Var クラスを考えよう。

struct Var
{
    Var(std::string name) : name(name), value() {}
    std::string const name;
    float value;
};

C++ クラス Var とそのデータメンバは次のようにして Python へエクスポートできる。

class_<Var>("Var", init<std::string>())
    .def_readonly("name", &Var::name)
    .def_readwrite("value", &Var::value);

これで Python 側で hello 名前空間内に Var クラスがあるように扱うことができる。

>>> x = hello.Var('pi')
>>> x.value = 3.14
>>> print x.name, 'is around', x.value
pi is around 3.14

name読み取り専用としてエクスポートしたいっぽうで、value読み書き可能としてエクスポートしたことに注意していただきたい。

>>> x.name = 'e' # name は変更できない
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: can't set attribute

クラスプロパティ

C++ では、公開データメンバを持つクラスは受け入れられない。カプセル化を利用して適切に設計されたクラスは、クラスのデータメンバを隠蔽しているものである。クラスのデータにアクセスする唯一の方法はアクセス関数(getter および setter)を介したものである。アクセス関数はクラスのプロパティをエクスポートする。以下がその例である。

struct Num
{
    Num();
    float get() const;
    void set(float value);
    ...
};

しかしながら Python における属性アクセスは優れたものである。ユーザが属性を直接処理しても、必ずしもカプセル化が破壊されるわけではない。属性はメソッド呼び出しの別の構文だからである。Num クラスを Boost.Python を使ってラップすると次のようになる。

class_<Num>("Num")
    .add_property("rovalue", &Num::get)
    .add_property("value", &Num::get, &Num::set);

これで Python 側は以下のようになる。

>>> x = Num()
>>> x.value = 3.14<
>>> x.value, x.rovalue
(3.14, 3.14)
>>> x.rovalue = 2.17 # エラー!

以下のように rovalue の setter メンバ関数を渡していないため、クラスのプロパティ rovalue は読み取り専用としてエクスポートされることに注意していただきたい。

.add_property("rovalue", &Num::get)

継承

これまでの例では多態的でないクラスを扱ってきたが、通常、そうしたことはあまりない。多くの場合、多態的なクラスや継承が絡んだクラス階層をラップすることになるだろう。仮想基底クラスから派生したクラスについて Boost.Python ラッパを書かなければならなくなるだろう。

次のような簡単な継承構造を考えよう。

struct Base { virtual ~Base(); };
struct Derived : Base {};

BaseDerived インスタンスを処理する C++ 関数群もあるとする。

void b(Base*);
void d(Derived*);
Base* factory() { return new Derived; }

基底クラス Base をラップする方法は以前見た。

class_<Base>("Base")
    /*...*/
    ;

Derived とその基底クラスである Base の関係について Boost.Python に伝える。

class_<Derived, bases<Base> >("Derived")
    /*...*/
    ;

これで自動的に以下の効果が得られる:

  1. DerivedBase のすべての Python メソッド(ラップされた C++ メンバ関数)を自動的に継承する。

  2. Base が多態的ならばBase へのポインタか参照で Python へ渡した Derived オブジェクトは、Derived へのポインタか参照が期待されているところに渡すことができる。

次に C++ 自由関数 bd および factory をエクスポートする。

def("b", b);
def("d", d);
def("factory", factory);

自由関数 factory が、Derived クラスの新しいインスタンスを生成するために使われることに注意していただきたい。このような場合は return_value_policy<manage_new_object> を使って、Base へのポインタを受け入れ、Python のオブジェクトが破壊されるまでインスタンスを新しい Python の Base オブジェクトに保持しておくことを Python に伝える。Boost.Python の呼び出しポリシーについては後で詳しく述べる。

// factory の結果について所有権を取るよう Python に伝える
def("factory", factory,
    return_value_policy<manage_new_object>());

仮想関数

本節では仮想関数を使って関数に多態的な振る舞いをさせる方法について学ぶ。前の例に引き続き、Base クラスに仮想関数を 1 つ追加しよう。

struct Base
{
    virtual ~Base() {}
    virtual int f() = 0;
};

Boost.Python の目標の 1 つが、既存の C++ の設計に対して侵入を最小限にすることである。原則的にはサードパーティ製ライブラリに対して、インターフェイス部分を変更することなくエクスポート可能であるべきである。Base クラスに何かを追加するのは望ましいことではない。しかし Python 側でオーバーライドし C++ から多態的に呼び出す関数の場合、正しく動作させるのに足場が必要になる。Python のオーバーライドが呼び出されるように仮想関数に非侵入的にフックする、Base から派生したラッパクラスを書くことである。

struct BaseWrap : Base, wrapper<Base>
{
    int f()
    {
        return this->get_override("f")();
    }
};

Base の継承に加え、wrapper<Base> を多重継承していることに注意していただきたい(ラッパの節を見よ)。wrapper テンプレートはラップするクラスを Python 側でオーバーライドできるようにする段取りを容易にする。

注意

msvc6/7 におけるバグの回避方法

Microsoft Visual C++ のバージョン 6 か 7 を使っている場合、f は次のように書かなければならない。

return call<int>(this->get_override("f").ptr());

BaseWrap のオーバーライドされた仮想メンバ関数 f は、実際には get_override を介して Python オブジェクトの相当するメソッドを呼び出す。

最後に Base をエクスポートする。

class_<BaseWrap, boost::noncopyable>("Base")
    .def("f", pure_virtual(&Base::f))
    ;

pure_virtual は、関数 f が純粋仮想関数であることを Boost.Python に伝える。

注釈

メンバ関数とメソッド

Python をはじめ、多くのオブジェクト指向言語ではメソッド(methods)という用語を使う。メソッドは大雑把に言えば C++ のメンバ関数(member functions)に相当する。

既定の実装をもった仮想関数

前節で Boost.Python のクラスラッパ機能を用いて純粋仮想関数を持ったクラスをラップする方法を見てきた。純粋仮想関数をラップする場合、方法は少し異なる。

前節を思い出そう。C++ で実装するか Python で派生クラスを作成する、純粋仮想関数を持ったクラスをラップした。基底クラスは次のように純粋仮想関数 f を持っていた。

struct Base
{
    virtual int f() = 0;
};

しかしながら、仮にメンバ関数 f が純粋仮想関数として宣言されていなかったら、

struct Base
{
    virtual ~Base() {}
    virtual int f() { return 0; }
};

以下のようにラップする。

struct BaseWrap : Base, wrapper<Base>
{
    int f()
    {
        if (override f = this->get_override("f"))
            return f(); // *注意*
        return Base::f();
    }

    int default_f() { return this->Base::f(); }
};

BaseWrap::f の実装方法に注意していただきたい。この場合、f のオーバーライドが存在するかチェックしなければならない。存在しなければ Base::f を呼び出すとよい。

注意

MSVC6/7 におけるバグの回避方法

Microsoft Visual C++ のバージョン 6 か 7 を使っている場合、*注意*と書いた行を次のように変更しなければならない。

return call<char const*>(f.ptr());

最後にエスクポートを行う。

class_<BaseWrap, boost::noncopyable>("Base")
    .def("f", &Base::f, &BaseWrap::default_f)
    ;

&Base::f&BaseWrap::default_f の両方をスクスポートしていることに注意していただきたい。Boost.Python は(1)転送(dispatch)関数fと(2)既定の実装への転送(forwarding)関数 default_f の追跡を維持しなければならない。この目的のための特別な def 関数が用意されている。

Python 側では結果的に次のようになる。

>>> base = Base()
>>> class Derived(Base):
...     def f(self):
...         return 42
...
>>> derived = Derived()

base.f を呼び出すと次のようになる。

>>> base.f()
0

derived.f を呼び出すと次のようになる。

>>> derived.f()
42

演算子・特殊関数

Python の演算子

C は演算子が豊富なことでよく知られている。C++ はこれを演算子の多重定義を認めることにより極限まで拡張した。Boost.Python はこれを利用して、演算子を多用した C++ クラスのラップを容易にする。

ファイルの位置を表すクラス FilePos と、FilePos インスタンスをとる演算子群を考える。

class FilePos { /*...*/ };

FilePos     operator+(FilePos, int);
FilePos     operator+(int, FilePos);
int         operator-(FilePos, FilePos);
FilePos     operator-(FilePos, int);
FilePos&    operator+=(FilePos&, int);
FilePos&    operator-=(FilePos&, int);
bool        operator<(FilePos, FilePos);

これらのクラスと演算子群は幾分簡単かつ直感的に Python へエクスポートできる。

class_<FilePos>("FilePos")
    .def(self + int())          // __add__
    .def(int() + self)          // __radd__
    .def(self - self)           // __sub__
    .def(self - int())          // __sub__
    .def(self += int())         // __iadd__
    .def(self -= other<int>())
    .def(self < self);          // __lt__

上記のコード片は非常に明確であり、ほとんど説明不要である。演算子のシグニチャと実質同じである。<constant>self</constant> が FilePos オブジェクトを表すということにのみ注意していただきたい。また、演算子式に現れるクラス T がすべて(容易に)デフォルトコンストラクト可能であるとは限らない。「self 式」を書くときに実際の T インスタンスの代わりに other<T>() が使える。

特殊メソッド

Python には他にいくつか特殊メソッドがある。Boost.Python は、実際の Python クラスインスタンスがサポートする標準的な特殊メソッド名をすべてサポートする。直感的なインターフェイス群で、これらの Python 特殊関数に相当する C++ 関数をラップできる。以下に例を示す。

class Rational
{ public: operator double() const; };

Rational pow(Rational, Rational);
Rational abs(Rational);
ostream& operator<<(ostream&,Rational);

class_<Rational>("Rational")
    .def(float_(self))                  // __float__
    .def(pow(self, other<Rational>))    // __pow__
    .def(abs(self))                     // __abs__
    .def(str(self))                     // __str__
    ;

他に言うことは?

注釈

operator<< の役割は? メソッド str が動作するために operator<< が必要なのだ(operator<<def(str(self)) が定義するメソッドが使用する)。

関数

本章では、Boost.Python の強力な関数について詳細を見る。懸垂ポインタや懸垂参照のような潜在的な落とし穴を避けつつ、C++ 関数を Python へエクスポートするための機能について見ていく。また、多重定義や既定の引数といった C++ 機能を利用した C++ 関数のエクスポートを容易にする機能についても見ていく。

先を続けよう。

しかしその前に Python 2.2 以降を立ち上げて >>> import this とタイプしたくなるかもしれない。

>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

呼び出しポリシー

C++ では引数や戻り値の型としてポインタや参照を扱うことがよくある。これら単純型は非常に低水準であり表現力に乏しい。少なくとも、ポインタや参照先のオブジェクトの所有権がどこにあるか知る方法はない。もっとも、Java や Python といった言語ではそのような低水準な実体を扱うことはない。C++ では、所有権のセマンティクスを正確に記述するスマートポインタの使用をよい慣習であると考えることが多い。それでも生の参照やポインタを使う C++ インターフェイスがよいとされる場合もあり、Boost.Python がそれらに対処できなければならない。このためには、あなたの助けが必要である。次のような C++ 関数を考える。

X& f(Y& y, Z* z);

ライブラリはこの関数をどのようにラップすべきだろうか? 単純なアプローチとしては、返される参照について Python の X オブジェクトを構築することである。この解法は動作する場合もあるが、動作しないこともある。以下が後者の例である。

>>> x = f(y, z) # x は C++ の X を参照する
>>> del y
>>> x.some_method() # クラッシュ!

何が起きたのか?

実は f が次のように実装されていたのだった。

X& f(Y& y, Z* z)
{
    y.z = z;
    return y.x;
}

問題は、f がオブジェクト y のメンバへの参照を返すため、結果の X& の寿命が y の寿命に縛られることである。このイディオムは珍しいものではなく、C++ の文脈では完全に受け入れられるものである。しかしながら Python のユーザとしてはこの C++ インターフェイスを使用するだけでシステムをクラッシュさせるわけにはいかない。今回の場合、y を削除した段階で X への参照が無効となり、懸垂参照が残るのである。

以下のようなことが起こっている。

  1. y への参照と z へのポインタを渡して f が呼び出される

  2. y.x への参照が返される

  3. y が削除される。x は懸垂参照となる

  4. x.some_method() が呼び出される

  5. バン!

結果を新しいオブジェクトにコピーしてみる。

>>> f(y, z).set(42) # 結果を消失
>>> y.x.get()       # クラッシュしないが、改善の余地がある
3.14

これは今回の C++ インターフェイスで本当に実現したかったことではない。Python インターフェイスが可能な限り綿密に C++ インターフェイスを反映すべきであるという約束を破っている。

問題はこれで終わりではない。Y の実装が次のようになっているとしたら、

struct Y
{
    X x; Z* z;
    int z_value() { return z->value(); }
};

データメンバ z がクラス Y に生のポインタで保持されていることに注意していただきたい。潜在的な懸垂ポインタの問題が Y の内部で発生している。

>>> x = f(y, z) # y は z を参照する
>>> del z       # オブジェクト z を削除
>>> y.z_value() # クラッシュ!

参考のために f の実装を再掲する。

X& f(Y& y, Z* z)
{
    y.z = z;
    return y.x;
}

以下のようなことが起こっている。

  1. y への参照と z へのポインタを渡して f が呼び出される

  2. yz へのポインタを保持する

  3. y.x への参照が返される

  4. z が削除される。y.z は懸垂ポインタとなる

  5. y.z_value() が呼び出される

  6. z->value() が呼び出される

  7. バン!

呼び出しポリシー

上で扱った例のような状況では、呼び出しポリシーが使える。今回の例では return_internal_referencewith_custodian_and_ward が助けになる。

def("f", f,
    return_internal_reference<1,
        with_custodian_and_ward<1, 2> >());

引数の 1 とか 2 って何だい?

return_internal_reference<1

これは「1 番目の引数(Y & y)が、返される参照(X&)の所有者である」と Boost.Python に伝えている。「1」は単に 1 番目の引数という意味である。まとめると「第 1 引数 Y & y が所有する内部参照 X& を返す」となる。

with_custodian_and_ward<1, 2>

これは「ward(被後見人)で指定した引数(第 2 引数。Z * z)の寿命が、custodian(後見人)で指定した引数(第 1 引数。Y & y)の寿命に依存する」と Boost.Python に伝えている。

上で 2 つのポリシーを定義していることに注意していただきたい。2 つ以上のポリシーは数珠繋ぎに結合できる。汎用的な構文は以下のようになる。

policy1<args...,
    policy2<args...,
        policy3<args...> > >

定義済みの呼び出しポリシーを以下のリストに挙げる。完全なリファレンスはここにある。

with_custodian_and_ward

引数の寿命を他の引数で縛る

with_custodian_and_ward_postcall

引数の寿命を他の引数や返り値で縛る

return_internal_reference

1 つの引数の寿命を返り値の寿命で縛る

return_value_policy<T>T は以下のいずれか)
reference_existing_object

単純(で危険)なアプローチ

copy_const_reference

Boost.Python v1 のアプローチ

copy_non_const_reference

manage_new_object

ポインタを受け取りインスタンスを保持する

禅(Zen)を思い出そう、Luke 1

「ごちゃごちゃ難しいのより、白黒はっきりしてるのがいい」

「あいまいなことをてきとーに処理しちゃいけません」

多重定義

多重定義したメンバ関数を手動でラップする方法を以下に示す。非メンバ関数の多重定義をラップする場合も、当然同様のテクニックが使える。

次のような C++ クラスを考える。

struct X
{
    bool f(int a)
    {
        return true;
    }

    bool f(int a, double b)
    {
        return true;
    }

    bool f(int a, double b, char c)
    {
        return true;
    }

    int f(int a, int b, int c)
    {
        return a + b + c;
    };
};

クラス X に多重定義された関数が 4 つある。まずメンバ関数ポインタ変数を導入するところから始める。

bool    (X::*fx1)(int)              = &X::f;
bool    (X::*fx2)(int, double)      = &X::f;
bool    (X::*fx3)(int, double, char)= &X::f;
int     (X::*fx4)(int, int, int)    = &X::f;

これがあれば、続けて Python のために定義とラップができる。

.def("f", fx1)
.def("f", fx2)
.def("f", fx3)
.def("f", fx4)

既定の引数

Boost.Python は(メンバ)関数ポインタをラップするが、残念ながら C++ 関数ポインタは既定の引数について情報を持たない。既定の引数を持った関数 f を考える。

int f(int, double = 3.14, char const* = "hello");

しかし関数 f へのポインタ型は、その既定の引数について情報を持たない。

int(*g)(int,double,char const*) = f;    // 既定の引数が失われる!

この関数ポインタを def 関数へ渡すとしても、既定の引数を取得する方法はない。

def("f", f);                            // 既定の引数が失われる!

このため C++ ラップコードを書くときは、前節で示したような手動のラップか薄いラッパを書くことに頼るしかない。

// 「薄いラッパ」を書く
int f1(int x) { return f(x); }
int f2(int x, double y) { return f(x,y); }

/*...*/

    // init モジュール内
    def("f", f);  // 3 引数バージョン
    def("f", f2); // 2 引数バージョン
    def("f", f1); // 1 引数バージョン

以下のいずれかの関数(、メンバ関数)をラップするときは、次節に進むとよい。

  • 既定の引数を持つ

  • 引数の先頭部分に共通列を持つ形で多重定義されている

BOOST_PYTHON_FUNCTION_OVERLOADS

Boost.Python はこれを容易にする方法を提供する。例えば次の関数が与えられたとする。

int foo(int a, char b = 1, unsigned c = 2, double d = 3)
{
    /*...*/
}

次のマクロ呼び出しにより、薄いラッパが作成される。

BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 1, 4)

このマクロは、def(...) に渡すことができる foo_overloads クラスを作成する。このマクロの 3 番目と 4 番目の引数は、それぞれ引数の最小数と最大数である。foo 関数では引数の最小数は 1 、最大数は 4 である。def(...) 関数は foo のファミリをすべて自動的に追加する。

def("foo", foo, foo_overloads());

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS

オブジェクトはここにも、そこにも、あそこにも、どこにでもある。Python にエクスポートするのは、クラスのメンバ関数が最も頻度が高い。ここでまた、以前の既定の引数や引数の先頭部分が共通列である多重定義の場合の不便が出てくる。これを容易にするマクロが提供されている。

BOOST_PYTHON_FUNCTION_OVERLOADS と同様、メンバ関数をラップする薄いラッパを自動的に作成するのに BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS を使用する。例を挙げる。

struct george
{
    void
    wack_em(int a, int b = 0, char c = 'x')
    {
        /*...*/
    }
};

ここで次のようにマクロを呼び出すと、

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(george_overloads, wack_em, 1, 3)

georgewack_em メンバ関数について最少で 1 、最多で 3(マクロの 3 番目と 4 番目の引数)の薄いラッパ群を生成する。薄いラッパはすべて george_overloads という名前のクラスに収められ、def(...) に引数として渡すことができる。

.def("wack_em", &george::wack_em, george_overloads());

詳細は多重定義のリファレンスを見よ。

init と optional

クラスのコンストラクタ、特に既定の引数と多重定義については類似の機能が提供されている。init<...> を覚えているだろうか? 例えばクラス X とそのコンストラクタがあるとすると、

struct X
{
    X(int a, char b = 'D', std::string c = "constructor", double d = 0.0);
    /*...*/
}

このコンストラクタを一発で Boost.Python に追加するには、

.def(init<int, optional<char, std::string, double> >())

init<...>optional<...> の使うことで、既定(省略可能な引数)であることを表現している点に注意していただきたい。

自動多重定義

前節で BOOST_PYTHON_FUNCTION_OVERLOADS および BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS が、引数列の先頭部分が共通である多重定義関数およびメンバ関数に対しても使用できることを見た。以下に例を示す。

void foo()
{
   /*...*/
}

void foo(bool a)
{
   /*...*/
}

void foo(bool a, int b)
{
   /*...*/
}

void foo(bool a, int b, char c)
{
   /*...*/
}

前節と同様、これらの多重定義された関数について薄いラッパを一発で生成できる。

BOOST_PYTHON_FUNCTION_OVERLOADS(foo_overloads, foo, 0, 3)

その結果、次のように書ける。

.def("foo", (void(*)(bool, int, char))0, foo_overloads());

この例では引数の個数は最少で 0 、最多で 3 となっていることに注意していただきたい。

手動のラッピング

多重定義した関数は引数列の先頭に共通部分を持っていなければならないということを強調しておく。それ以外の場合、上で述べた方法は動作せず、関数を手動でラップしなければならない。

実際には多重定義関数の手動ラッピングと、BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS とその姉妹版である BOOST_PYTHON_FUNCTION_OVERLOADS による自動的なラッピングを混用することは可能である。多重定義の節で見た例だと 4 つの多重定義関数は引数の先頭列が共通であるので、BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS を使って最初の 3 つの def を自動的にラップでき、残り 1 つだけを手動でラップすることになる。以下のようにする。

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(xf_overloads, f, 1, 4)

両方の X::f 多重定義について、以前と同様にメンバ関数ポインタを作成すると、

bool    (X::*fx1)(int, double, char)    = &X::f;
int     (X::*fx2)(int, int, int)        = &X::f;

結果、以下のように書ける。

.def("f", fx1, xf_overloads());
.def("f", fx2)

オブジェクトのインターフェイス

C++ が静的型付けであるのに対し、Python は動的型付けである。Python の変数は整数、浮動小数点数、リスト、辞書、タプル、文字列、長整数、その他を保持できる。Boost.Python と C++ の視点では、これら Python 的な変数は object クラスのインスタンスにすぎない。本章で Python のオブジェクトをどのように扱うか見ていく。

以前述べたように Boost.Python の目的の 1 つは、C++ と Python 間における Python 的な感覚の双方向マッピングの提供である。Boost.Python における C++ の object は可能な限り Python に類似したものとなっている。これにより学習曲線は著しく最小化されるはずである。

基本的なインターフェイス

object クラスは PyObject* をラップする。参照カウントの管理といった PyObject の複雑な取り扱いは、すべて object クラスが処理する。C++ オブジェクトの相互運用性はシームレスなものである。実際のところ、Boost.Python における C++ の object はあらゆる C++ オブジェクトから明示的に構築できる。

説明のために、以下のような Python コード片を考える。

def f(x, y):
     if (y == 'foo'):
         x[3:7] = 'bar'
     else:
         x.items += y(3, x)
     return x

def getfunc():
   return f

Boost.Python の機能を用いて C++ で書き直すと次のようになる。

object f(object x, object y) {
     if (y == "foo")
         x.slice(3,7) = "bar";
     else
         x.attr("items") += y(3, x);
     return x;
}
object getfunc() {
    return object(f);
}

C++ でコードを書いているという外観的な差を除けば、そのルックアンドフィールは Python のプログラマにも明確である。

object の派生型

Boost.Python には、Python の各型に対応する object の派生型がある。

  • list

  • dict

  • tuple

  • str

  • long_

  • enum_

これらの object の派生型は実際の Python 型と同様に振舞う。例を挙げる。

str(1) ==> "1"

個々の派生 object は対応する Python 型のメソッドを持つ。例えば dictkeys メソッドを持つ。

d.keys()

タプルリテラルを宣言するのに make_tuple が提供されている。例を挙げる。

make_tuple(123, 'D', "Hello, World", 0.0);

C++ において、Boost.Python の object を関数の引数に渡す場合は派生型の一致が要求される。例えば以下に示す関数 f をラップする場合、Python の str 型とその派生型のみを受け付ける。

void f(str name)
{
    object n2 = name.attr("upper")();   // NAME = name.upper()
    str NAME = name.upper();            // のほうがよい
    object msg = "%s is bigger than %s" % make_tuple(NAME,name);
}

細かく見ると、

str NAME = name.upper();

このコードから分かるように、str 型のメソッドを C++ メンバ関数として提供している。次に、

object msg = "%s is bigger than %s" % make_tuple(NAME,name);

上記のコードのように Python の "format" % x,y,z を C++ で書ける。標準の C++ で同じことを簡単に行う方法がないため便利である。

注意

Python 同様、Python の可変型の多くがコンストラクタでコピーを行うというよく知られた落とし穴があるので注意が必要である。

Python の場合:

>>> d = dict(x.__dict__)     # x.__dict__ をコピーする
>>> d['whatever'] = 3        # コピーを変更する

C++ の場合:

dict d(x.attr("__dict__"));  // x.__dict__ をコピーする
d["whatever"] = 3;           // コピーを変更する

object としての class_<T>

Boost.Python における object の動的な性質に従えば、あらゆる class_<T> もまたこれら型の 1 つである! 以下のコード片はクラス(型)オブジェクトをラップする。

これを使って、ラップされたインスタンスを作成できる。

object vec345 = (
    class_<Vec2>("Vec2", init<double, double>())
        .def_readonly("length", &Point::length)
        .def_readonly("angle", &Point::angle)
    )(3.0, 4.0);

assert(vec345.attr("length") == 5.0);

C++ オブジェクトの抽出

object インスタンスを使用せずに C++ の値が必要になることがある。これは extract<T> 関数で実現できる。以下を考える。

double x = o.attr("length"); // コンパイルエラー

Boost.Python の objectdouble へ暗黙に変換できないため、上記のコードはコンパイルエラーとなる。代わりに以下のように書けば希望どおりとなる。

double l = extract<double>(o.attr("length"));
Vec2& v = extract<Vec2&>(o);
assert(l == v.length());

1 行目は Boost.Python の objectlength 属性を抽出しようとしている。2 行目は Boost.Python の object が保持している Vec2 オブジェクトを抽出しようとしている。

「~しようとしている」と書いたことに注意していただきたい。Boost.Python の object が実際には Vec2 型を保持していなかったらどうなるだろうか? これは Python の object がもつ動的な性質を考えれば十分ありうることである。安全のため、希望する C++ 型を抽出できない場合は適当な例外が投げられる。例外を避けるには抽出できるかテストする必要がある。

extract<Vec2&> x(o);
if (x.check()) {
    Vec2& v = x(); ...

明敏な読者は extract<T> の機能が変更可能コピーの問題を解決することに気付いたかもしれない。

dict d = extract<dict>(x.attr("__dict__"));
d["whatever"] = 3;          // x.__dict__ を変更する!

列挙

Boost.Python には、C++ の列挙を捕捉、ラップする気の利いた機能がある。Python に enum 型はないが、C++ の列挙を Python へ int としてエクスポートしたいことがよくある。Python の動的型付けから C++ の強い静的型付けへの適切な変換に気を付けていれば Boost.Python の列挙機能で容易に可能である(C++ では、整数から列挙へ暗黙に変換することはできない)。次のような C++ の列挙があったとして、

enum choice { red, blue };

次のようにして Python へエクスポートする。

enum_<choice>("choice")
    .value("red", red)
    .value("blue", blue)
    ;

新しい列挙は現在の scope に作成される。これは大抵の場合現在のモジュールである。上記のコード片は Python の int 型から派生した、第 1 引数に渡した C++ 型に対応する Python クラスを作成する。

注釈

scope とは

scope は、新しい拡張クラスやラップした関数が属性として定義される Python の名前空間を制御するグローバルな関連 Python オブジェクトを持つクラスである。詳細はリファレンスを見よ。

Python からはこれらの値に以下のようにしてアクセスできる。

>>> my_module.choice.red
my_module.choice.red

ここで my_module は列挙を宣言したモジュールである。新しいスコープをクラスに対して作成することもできる。

scope in_X = class_<X>("X")
                .def( ... )
                .def( ... )
            ;

// X::nested を X.nested としてエクスポートする
enum_<X::nested>("nested")
    .value("red", red)
    .value("blue", blue)
    ;

PyObject* から boost::python::object を作成する

PyObject* である pyobj へのポインタを boost::python::object で管理したい場合、以下のようにする。

boost::python::object o(boost::python::handle<>(pyobj));

この場合、オブジェクト opyobj を管理するが、構築時に参照カウントを増やさない。

あるいは借用(borrowed)参照を使う方法として、

boost::python::object o(boost::python::handle<>(boost::python::borrowed(pyobj)));

この場合 Py_INCREF が呼び出されるので、オブジェクト o がスコープ外に出ても pyobj は破壊されない。

組み込み

Boost.Python を使って Python から C++ のコードを呼び出す方法について理解できたと思う。しかしときには逆のこと、つまり C++ 側から Python のコードを呼び出す必要が出てくるはずである。これには Python のインタープリタを C++ のプログラムに組み込む必要がある。

現時点では Boost.Python は組み込みに必要なことをすべてサポートしているわけではない。したがってこのギャップを埋めるには Python の C API を使う必要が出てくる。とはいえ Boost.Python は組み込みの大部分を容易にしており、将来のバージョンでは Python の C API に触れる必要はなくなるかもしれない。そういうわけだから期待しておいて欲しい。

組み込みプログラムをビルドする

Python をプログラムに組み込み可能にするには、Python だけでなく Boost.Python 本体の実行時ライブラリにもリンクしなければならない。

Boost.Python のライブラリは 2 種類ある。いずれも Boost の /libs/python/build/bin-stage サブディレクトリにある。Windows ではライブラリの名前は boost_python.lib(リリースビルド用)と boost_python_debug.lib(デバッグ用)である。ライブラリが見つからない場合は、おそらくまだ Boost.Python をビルドしていないのだろう。ビルドとテストを見て方法を確認するとよい。

Python のライブラリは、Python ディレクトリの /libs サブディレクトリにある。Windows では pythonXY.lib のような名前で、X.Y が Python のメジャーバージョンの番号である。

また Python の /include サブディレクトリをインクルードパスに追加しておかなければならない。

Jamfile に以上のことをすべて要約すると、

projectroot c:\projects\embedded_program ; # プログラムの場所

# Python 用の規則
SEARCH on python.jam = $(BOOST_BUILD_PATH) ;
include python.jam ;

exe embedded_program # 実行可能ファイルの名前
  : # ソースファイル
     embedded_program.cpp
  : # 必須条件
     <find-library>boost_python <library-path>c:\boost\libs\python
  $(PYTHON_PROPERTIES)
    <library-path>$(PYTHON_LIB_PATH)
    <find-library>$(PYTHON_EMBEDDED_LIBRARY) ;

はじめに

ビルドできるようになったのはよいが、まだビルドするものがない。Python のインタープリタを C++ のプログラムに組み込むには、以下の 3 段階が必要である。

  1. <boost/python.hpp> をインクルードする。

  2. Py_Initialize <http://docs.python.jp/2/c-api/init.html#Py_Initialize> を呼び出してインタープリタを起動、__main__ モジュールを作成する。

  3. 他の Python C API を呼び出してインタープリタを使用する。

注釈

現時点ではインタープリタを停止するのに Py_Finalize <http://docs.python.jp/2/c-api/init.html#Py_Finalize> を呼び出してはならない。これは Boost.Python の将来のバージョンで修正する。

(当然ながら、上記の各段階の間に他の C++ コードを挟んでもよい。)

これでプログラムにインタープリタを組み込み可能になった。次に使用方法を見ていく。

インタープリタを使用する

すでに知っていることと思うが、Python のオブジェクトは参照カウントで管理されている。当然、Python C API の PyObject も参照カウンタを持っているが、違いがある。参照カウントは Python では完全に自動で行われているが、Python C API では手動で行う必要がある。これは厄介で、とりわけ C++ 例外が現れるコードで正しく取り扱うのが困難である。幸いにも Boost.Python には handle および object クラステンプレートがあり、この処理を自動化できる。

Python のコードを起動する

Boost.Python は、C++ から Python のコードを起動する関数を 3 つ提供している。

object eval(str expression, object globals = object(), object locals = object())
object exec(str code, object globals = object(), object locals = object())
object exec_file(str filename, object globals = object(), object locals = object())

eval は与えられた式を評価し結果の値を返す。exec は与えられたコード(典型的には文の集まり)を実行し結果を返す。exec_file は与えられたファイル内のコードを実行する。

これらについては第 1 引数が str ではなく char* const となっている多重定義もある。

globalslocals 引数は、コードを実行するコンテキストの globalslocals に相当する Python の辞書である。ほとんどの目的において、__main__ モジュールの名前空間辞書を両方の引数に使用するとよい。

Boost.Python はモジュールをインポートする関数を提供する。

object import(str name)

import は Python のモジュールをインポートし(潜在的には、はじめに起動しているプロセスに読み込む)、返す。

__main__ モジュールをインポートし、その名前空間で Python のコードを走らせてみよう。

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");

object ignored = exec("hello = file('hello.txt', 'w')\n"
                      "hello.write('Hello world!')\n"
                      "hello.close()",
                      main_namespace);

このコードは現在のディレクトリに hello.txt という名前のファイルを作成し、プログラミングサークルでよく知られたフレーズを書き込む。

Python のオブジェクトを操作する

Python オブジェクトを操作するクラスが欲しくなることがよくある。しかしすでに上記や前節でそのようなクラスを見た。文字通りの名前を持つ object とその派生型である。またそれらを handle から構築できることも見た。以下の例を見ればより明らかだろう。

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
object ignored = exec("result = 5 ** 2", main_namespace);
int five_squared = extract<int>(main_namespace["result"]);

__main__ モジュールの名前空間に相当する辞書オブジェクトを作成している。次に 5 の 2 乗を結果の変数に代入し、この変数を辞書から読んでいる。同じ結果を得る他の方法としては代わりに eval を使用する方法があり、こちらは結果を直接返す。

object result = eval("5 ** 2");
int five_squared = extract<int>(result);

例外処理

Python の式を評価中に例外を送出した場合、error_already_set が投げられる。

try
{
    object result = eval("5/0");
    // ここには絶対に来ない:
    int five_divided_by_zero = extract<int>(result);
}
catch(error_already_set const &)
{
    // 何らかの方法で例外を処理する
}

error_already_set 例外クラス自体は何の情報も持たない。送出された Python の例外について詳細を調べるには、catch 文内で Python C API の例外処理関数を使用する必要がある。これは単純に PyErr_Print() を呼び出して例外のトレースバックをコンソールへプリントするか、あるいは例外の型を標準の例外と比較する程度となるだろう。

catch(error_already_set const &)
{
    if (PyErr_ExceptionMatches(PyExc_ZeroDivisionError))
    {
        // ZeroDivisionError を特別扱いする
    }
    else
    {
        // 他のすべてのエラーを stderr にプリントする
        PyErr_Print();
    }
}

(例外についてより多くの情報を取得するには、このリストにある例外処理関数を使用する。)

イテレータ

C++ 、特に STL においてイテレータはあらゆる場面で使用されている。Python にもイテレータがあるが、両者には大きな違いがある。

C++ のイテレータ:
  • C++ のイテレータは 5 つに分類される(ランダムアクセス、双方向、単方向、入力、出力)

  • 再配置とアクセスの 2 種類の操作がある

  • 範囲を表すのにイテレータの組(先頭と末尾)が必要

Python のイテレータ:
  • 分類は 1 つしかない(単方向)

  • 操作は 1 種類しかない(next()

  • 終了時に StopIteration 例外を投げる

典型的な Python の走査プロトコルである for y in x... は以下のようである。

iter = x.__iter__()         # イテレータを取得する
try:
    while 1:
    y = iter.next()         # 各要素を取得する
    ...                     # y を処理する
except StopIteration: pass  # イテレータが尽きた

Boost.Python は、C++ のイテレータを Python のイテレータとして振舞うようにする機構をいくつか提供している。必要なことは C++ のイテレータから Python の走査プロトコルと互換性のある適切な __iter__ 関数を用意することである。例えば、

object get_iterator = iterator<vector<int> >();
object iter = get_iterator(v);
object first = iter.next();

あるいは class_<> で以下のようにする。

.def("__iter__", iterator<vector<int> >())

range

range 関数を使用すると、Python の実践的なイテレータを作成できる。

  • range(start, finish)

  • range<Policies, Target>(start, finish)

ここで startfinish は以下のいずれかである。

  • メンバデータポインタ

  • メンバ関数ポインタ

  • 関数オブジェクト(Target 引数を使用)

iterator

コンテナ T が与えられた場合、iterator は単に &T::begin&T::endrange を呼び出すショートカットとなる。

実際にやってみよう。以下はある仮説の粒子加速器のコードからの例である。

f = Field()
for x in f.pions:
    smash(x)
for y in f.bogons:
    count(y)

C++ のラッパは以下のようになるだろう。

class_<F>("Field")
    .property("pions", range(&F::p_begin, &F::p_end))
    .property("bogons", range(&F::b_begin, &F::b_end));

stl_input_iterator

ここまで C++ のイテレータと範囲を Python へエクスポートする方法を見てきた。しかしこれ以外に、Python のシーケンスを STL アルゴリズムに渡したり、STL コンテナを初期化したい場合がある。Python のイテレータを STL のイテレータのように見せかける必要がある。これには stl_input_iterator<> を使用する。std::list<int>::assign を Python へエクスポートする関数の実装方法を考えよう。

template<typename T>
void list_assign(std::list<T>& l, object o) {
    // Python のシーケンスを STL の入力範囲に変換する
    stl_input_iterator<T> begin(o), end;
    l.assign(begin, end);
}

// list<int> のラッパの一部
class_<std::list<int> >("list_int")
    .def("assign", &list_assign<int>)
    // ...
    ;

これで Python 側であらゆる整数シーケンスを list_int オブジェクトへ代入できる。

x = list_int();
x.assign([1,2,3,4,5])

例外の変換

C++ の例外はすべて Python コードとの境界で捕捉しなければならない。この境界は C++ が Python と接する地点である。Boost.Python は、選択した標準の例外を変換して処理を中止する既定の例外ハンドラを提供する。

raise RuntimeError, 'unidentifiable C++ Exception'

ユーザがカスタムの変換器を提供してもよい。例えば、2

struct PodBayDoorException;
void translator(PodBayDoorException const& x) {
    PyErr_SetString(PyExc_UserWarning, "I'm sorry Dave...");
}
BOOST_PYTHON_MODULE(kubrick) {
     register_exception_translator<
          PodBayDoorException>(translator);
     ...

典型的なテクニック

Boost.Python でコードをラップするのに使えるテクニックをいくつか紹介する。

パッケージを作成する

Python のパッケージは、ユーザに一定の機能を提供するモジュールの集まりである。パッケージの作成についてなじみがなければ、Python のチュートリアルによい導入がある。

しかし今は Boost.Python を使って C++ コードをラップしているのである。優れたパッケージインターフェイスをユーザに提供するにはどうすればよいだろうか? 概念的なことを捉えるために例を使って考えよう。

音に関する C++ ライブラリがあったとする。様々な形式で読み書きし、音データにフィルタをかける等するものとする。(便宜的に)名前を sounds としておこう。以下のような整理された C++ 名前空間の階層がすでにあるとする。

sounds::core
sounds::io
sounds::filters

Python ユーザに同じ階層を提示し、次のようなコードが書けるようにしたい。

import sounds.filters
sounds.filters.echo(...) # echo は C++ 関数

第 1 段階はラップコードを書くことである。以下のように Boost.Python を使って各モジュールを個別にエクスポートしなければならない。

/* ファイル core.cpp */
BOOST_PYTHON_MODULE(core)
{
    /* 名前空間 sounds::core 内のものをすべてエクスポートする */
    ...
}

/* ファイル io.cpp */
BOOST_PYTHON_MODULE(io)
{
    /* 名前空間 sounds::io 内のものをすべてエクスポートする */
    ...
}

/* ファイル filters.cpp */
BOOST_PYTHON_MODULE(filters)
{
    /* 名前空間 sounds::filters 内のものをすべてエクスポートする */
    ...
}

これらのファイルをコンパイルすると、core.pydio.pyd および filters.pyd の Python 拡張が生成される。

注釈

拡張子 .pyd は Python の拡張モジュールで使用するものであり、単純に共有ライブラリである。システムで既定のもの(Unix の場合は .so 、Windows の場合は .dll)を使用しても差し支えない。

次に以下の Python パッケージ用のディレクトリ構造を作成する。

sounds/
    __init__.py
    core.pyd
    filters.pyd
    io.pyd

ファイル __init__.py は、ディレクトリ sounds/ が実際は Python のパッケージであることを Python に伝える。このファイルは空でもよいが、後述するようにここでマジックを行うことも可能だ。

これでパッケージの準備が整った。ユーザがなすべきなのは、soundsPYTHONPATH に置いてインタープリタを起動することだけである。

>>> import sounds.io
>>> import sounds.filters
>>> sound = sounds.io.open('file.mp3')
>>> new_sound = sounds.filters.echo(sound, 1.0)

何も問題無いようだが、どうだろう?

これはパッケージ階層を作成する最も単純な方法だが、柔軟性がまるでない。純粋な Python の関数、例えば音オブジェクトに 3 つのフィルタを同時にかける関数を filters パッケージに追加したい場合はどうだろうか? 確かにC++ で書いてエクスポートすれば可能だが、Python でやってみてはどうか。そうすれば拡張モジュールの再コンパイルが不要で、書くのも簡単である。

こういった柔軟性が必要な場合、パッケージ階層を少しばかり複雑にしなければならない。まず拡張モジュール群の名前を変更しなければならない。

/* ファイル core.cpp */
BOOST_PYTHON_MODULE(_core)
{
    ...
    /* 名前空間 sounds::core 内のものをすべてエクスポートする */
}

モジュール名にアンダースコアを追加したことに注意していただきたい。ファイル名も _core.pyd に変わるはずである。他の拡張モジュールも同様である。これでパッケージ階層は以下のように変更された。

sounds/
    __init__.py
    core/
        __init__.py
        _core.pyd
    filters/
        __init__.py
        _filters.pyd
    io/
        __init__.py
        _io.pyd

各拡張モジュールについてディレクトリを作成し、それぞれに __init__.py を追加したことに注意していただきたい。しかしこれをこのままおいておくと、ユーザは次のような構文で core モジュールの関数にアクセスしなければならない。

>>> import sounds.core._core
>>> sounds.core._core.foo(...)

これは望ましいことではない。しかしここで __init__.py のマジックが発動する。__init__.py の名前空間に持ち込まれるものはすべてユーザが直接アクセスできるのである。そういうわけで、名前空間全体を _core.pyd から core/__init__.py へ持ち込むだけでよい。つまり次のコード行を sounds/core/__init__.py へ追加する。

from _core import *

他のパッケージも同様に行う。これでユーザは以前のように拡張モジュール内と関数とクラスにアクセスできるようになる。

>>> import sounds.filters
>>> sounds.filters.echo(...)

他にも純粋な Python 関数をあらゆるモジュールに容易に追加できるという利点もある。この方法であればユーザには C++ 関数と Python 関数の見分けが付かない。では純粋な Python 関数 echo_noisefilters パッケージに追加しよう。この関数は与えられた sound オブジェクトに echonoise の両方のフィルタを順番に適用する。sounds/filters/echo_noise.py という名前でファイルを作成して関数のコードを書く。

import _filters
def echo_noise(sound):
    s = _filters.echo(sound)
    s = _filters.noise(sound)
    return s

次に以下の行を sounds/filters/__init__.py に追加する。

from echo_noise import echo_noise

これで終わりだ。ユーザは、filters パッケージの他の関数と同様にこの関数にアクセスできる。

>>> import sounds.filters
>>> sounds.filters.echo_noise(...)

ラップしたオブジェクトを Python で拡張する

Python の柔軟性に感謝することだ。クラスを作成した後であってもメソッドを容易に追加できる。

>>> class C(object): pass
>>>>
>>>> # 普通の関数
>>>> def C_str(self): return 'C のインスタンス!'
>>>
>>> # メンバ関数に変更する
>>> C.__str__ = C_str
>>>
>>> c = C()
>>> print c
C のインスタンス!
>>> C_str(c)
C のインスタンス!

やはり Python は素晴らしい。

同様のことが Boost.Python でラップしたクラスでもできる。C++ 側に point クラスがあるとする。

class point {...};

BOOST_PYTHON_MODULE(_geom)
{
    class_<point>("point")...;
}

前節『パッケージを作成する』のテクニックを使うと geom/__init__.py に直接コードが書ける。

from _geom import *

# 普通の関数
def point_str(self):
    return str((self.x, self.y))

# メンバ関数に変更する
point.__str__ = point_str

C++ で作成したすべての point インスタンスがこのメンバ関数を持つことになる! このテクニックには色々と利点がある。

  • 追加する関数についてのコンパイル時間増加がゼロになる

  • メモリのフットプリントが見かけ上ゼロに削減する

  • 再コンパイルの必要が最小になる

  • 高速なプロトタイピング(インターフェイスを変更しないことが要求されている場合、コードを C++ に移動することが可能)

別の有用な考えとして、コンストラクタをファクトリ関数で置き換える方法がある。

_point = point

def point(x=0, y=0):
    return _point(x, y)

このような簡単な例ではつまらない感じがするが、多重定義や引数が多数あるコンストラクタにおいては優れた単純化となることが多い。キーワードサポートに対してコンパイル時間のオーバーヘッドがゼロ、メモリのフットプリントも事実上ゼロとなる。

コンパイルにかかる時間を短縮する

クラスを大量にエクスポートすると、Boost.Python ラッパのコンパイルにかなりの時間がかかる。またメモリの消費量が容易に過大となる。これが問題となるのであれば、class_ 定義を複数のファイルに分割するとよい。

/* ファイル point.cpp */
#include <point.h>
#include <boost/python.hpp>

void export_point()
{
    class_<point>("point")...;
}

/* ファイル triangle.cpp */
#include <triangle.h>
#include <boost/python.hpp>

void export_triangle()
{
    class_<triangle>("triangle")...;
}

そして BOOST_PYTHON_MODULE マクロを含んだ main.cpp ファイルを作成し、その中でエクスポート関数を呼び出す。

void export_point();
void export_triangle();

BOOST_PYTHON_MODULE(_geom)
{
    export_point();
    export_triangle();
}

これらのファイルをすべてコンパイル、リンクすると、通常の方法の場合と同じ結果が得られる。しかしメモリはまともな状態が維持できる。

#include <boost/python.hpp>
#include <point.h>
#include <triangle.h>

BOOST_PYTHON_MODULE(_geom)
{
    class_<point>("point")...;
    class_<triangle>("triangle")...;
}

C++ ライブラリ開発と Python へのエクスポートを同時に行っている場合にも、この方法を推奨する。クラス内で変更があっても、ラッパコード全体ではなく単一の cpp ファイルについてコンパイルが必要になるだけである。

注釈

巨大なソースファイルをコンパイルしてエラーメッセージ「致命的なエラー C1204:コンパイラの制限:内部構造がオーバーフローしました。」が出た場合にも、この方法を推奨する。FAQ に説明がある。

1

訳注 この日本語訳は http://www.python.jp/Zope/Zope/articles/misc/zen によりました(Copyright © 2001-2012 Python Japan User's Group)。

2

訳注 『2001 年宇宙の旅』(“2001: A Space Odyssey” : Stanley Kubrick and Arthur C. Clarke, 1968)かな?