チュートリアル
Joel de GuzmanDavid Abrahams© 2002-2005 Joel de Guzman, David AbrahamsDistributed 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.py
は hello.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.cpp
と Jamroot
が置いてある libs/python/example/tutorial
に cd で移動するのを忘れないように。
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<...>
を渡すことでエクスポートするコンストラクタを追加できる。例えば World
に double
を 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 {};
Base
と Derived
インスタンスを処理する 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")
/*...*/
;
これで自動的に以下の効果が得られる:
Derived
はBase
のすべての Python メソッド(ラップされた C++ メンバ関数)を自動的に継承する。Base
が多態的ならば、Base
へのポインタか参照で Python へ渡したDerived
オブジェクトは、Derived
へのポインタか参照が期待されているところに渡すことができる。
次に C++ 自由関数 b
、d
および 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
への参照が無効となり、懸垂参照が残るのである。
以下のようなことが起こっている。
y
への参照とz
へのポインタを渡してf
が呼び出されるy.x への参照が返される
y
が削除される。x
は懸垂参照となるx.some_method() が呼び出される
バン!
結果を新しいオブジェクトにコピーしてみる。
>>> 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;
}
以下のようなことが起こっている。
y
への参照とz
へのポインタを渡してf
が呼び出されるy
がz
へのポインタを保持するy.x への参照が返される
z
が削除される。y.z は懸垂ポインタとなるy.z_value() が呼び出される
z->value() が呼び出される
バン!
呼び出しポリシー
上で扱った例のような状況では、呼び出しポリシーが使える。今回の例では return_internal_reference
と with_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)
george
の wack_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 型のメソッドを持つ。例えば dict
は keys
メソッドを持つ。
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 の object
は double
へ暗黙に変換できないため、上記のコードはコンパイルエラーとなる。代わりに以下のように書けば希望どおりとなる。
double l = extract<double>(o.attr("length"));
Vec2& v = extract<Vec2&>(o);
assert(l == v.length());
1 行目は Boost.Python の object
の length
属性を抽出しようとしている。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));
この場合、オブジェクト o
は pyobj
を管理するが、構築時に参照カウントを増やさない。
あるいは借用(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 段階が必要である。
<boost/python.hpp>
をインクルードする。Py_Initialize <http://docs.python.jp/2/c-api/init.html#Py_Initialize>
を呼び出してインタープリタを起動、__main__
モジュールを作成する。他の 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
となっている多重定義もある。
globals
と locals
引数は、コードを実行するコンテキストの globals
と locals
に相当する 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)
ここで start
、finish
は以下のいずれかである。
メンバデータポインタ
メンバ関数ポインタ
関数オブジェクト(
Target
引数を使用)
iterator
iterator<T, Policies>()
コンテナ T
が与えられた場合、iterator
は単に &T::begin と &T::end で range
を呼び出すショートカットとなる。
実際にやってみよう。以下はある仮説の粒子加速器のコードからの例である。
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.pyd
、io.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 に伝える。このファイルは空でもよいが、後述するようにここでマジックを行うことも可能だ。
これでパッケージの準備が整った。ユーザがなすべきなのは、sounds
を PYTHONPATH に置いてインタープリタを起動することだけである。
>>> 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_noise
を filters
パッケージに追加しよう。この関数は与えられた sound
オブジェクトに echo
と noise
の両方のフィルタを順番に適用する。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)かな?