※ 以下では公式に倣ってnamespace py = pybind11;
しています。
TL;DR
Q. 異なる型を返すC++関数をまとめて一つの関数としてPythonにバインドし、状況に応じて返す型を変えたい
A. pybind11なら、py::cast
してpy::object
を返すラムダをバインドしちゃえばいいよ
例:読んだ画像フォーマットに応じて異なる構造体を返したい
// いくつかのピクセル型が用意されている
struct gray_pixel;
struct rgba_pixel;
// 渡されたピクセル型に対応する画像を読む関数が既にある
template<typename Pixel>
image<Pixel> read_image(const std::string& fname);
// Pythonからはファイル名を渡したら勝手に読んでどちらかのオブジェクトを返して欲しい
mod.def("read_image", [](const std::string& fname) -> py::object {
if (is_grayscale(fname)) {return py::cast(read_image<gray_pixel>(fname));}
else if(is_rgba(fname)) {return py::cast(read_image<rgba_pixel>(fname));}
else {throw std::runtime_error("invalid image format");}
}, py::arg("filename"), "reads image file.");
これで伝わったら以下は蛇足です。
背景
C++では関数の返却値型は決まっています。
result_type function(args_t ... args);
返却値の値そのものは変化するのが普通ですが、型は通常変わりません。そして、型を実行時に変えることはできません。型はコンパイル時に決まる必要があります。
なので、以下のように実行時に型が決まるようなコードはC++では書けません。
auto function(const bool flag)
{
if(flag)
{
return certain_type{/*...*/};
}
return another_type{/*...*/}; // できない
}
では、こうしたくなった場合はどうすればいいのでしょう。
一つには、継承を使って基底型のスマートポインタを返すという手があります。これはPythonのように、型情報(の一部)を実行時に決めることに相当します。
class base_type {virtual ~base_type() = default;};
class certain_type: base_type {/*...*/};
class another_type: base_type {/*...*/};
std::unique_ptr<base_type> function(const bool flag)
{
if(flag)
{
return std::make_unique<certain_type>(/*...*/);
}
return std::make_unique<another_type>(/*...*/);
}
この解決法が伝統的でかつよく用いられているやり方でしょう。オーバーロードされている、またはtemplateの関数が返す異なる型というのは概ね同じ系統に属するもののはずなので、継承を使ってもそんなに変なことにはならないでしょう。
他に、C++17で追加されたvariant
を使うこともできます。
std::variant<certain_type, another_type> function(const bool flag)
{
if(flag)
{
return certain_type{/*...*/};
}
return another_type{/*...*/};
}
この後ジェネリックな関数を使って処理を進める場合、戻ってきた値にstd::visit
(パターンマッチのようなもの)すれば同じくらい簡単に使えます。これも同様に型情報の一部を動的に決めているわけですが(std::variant::index
)、入り得る型を制限でき、またメモリ確保が静的にできるので効率が良くなる可能性があります。あとメンバがどの段階でオーバーライドされているかがわからなくなったりしません。
ところでPythonでは型は動的に決まるため、関数が動的に戻り値型を変えても何も不自然ではありません。
なので、Pythonへのバインディングを行う場合、以下のように振る舞うコードをPython側に見せたくなるはずです。
def function(arg):
if is_ok(arg):
return CertainObject(arg)
else:
return AnotherObject(arg)
さて、pybind11があります。C++関数をバインドしてモジュールに加えるには以下のようにするわけですが、
mod.def("function", &function);
function
の定義はどのようにすればいいでしょう?
解決策
さて、pybind11は優秀なので、std::variantはサポートされています。もちろんですが、継承もサポートされています。
では、それで話は終わったはずではないんですか?
解決されずに残っている問題は、既にC++ライブラリがあって、その設計時にPythonバインディングなんて何も考えていなかったのに、後でPythonにバインドしたいと思ってしまった時です。設計を変えて対応するのはフルスクラッチと変わらないので手間がかかり過ぎますし、既に問題を上手く解決できているならわざわざコードに追加の複雑さを持ち込みたくはありません。
例を考えてみましょう。C++で画像を読み書きするライブラリを書いていたとします。そしてそのライブラリは、ピクセルの情報量ごとに異なる型を用意していたとします。
struct gray_pixel {std::uint8_t value;};
struct rgba_pixel {std::uint8_t R, G, B, A;};
とすると、画像クラスも一つ一つ型が違うことになります。
template<typename pixelT>
class image;
using gray_image = image<gray_pixel>;
using rgba_image = image<rgba_pixel>;
そして、それぞれの読み込み関数は以下のようになるでしょう。
template<typename pixelT>
image<pixelT> read_image(const std::string& file_name);
C++的には、以下のようなユーザーコードが考えられます。
const auto img = read_image<rgba_pixel>("image.rgba");
const auto reversed = reverse(img);
const auto rotated = rotate(reversed, 90_degree);
write_image(rotated, "output.rgba");
image
クラスやピクセル型はバインドされているとして、次はこのread_image
関数をPythonにバインドしたいわけです。
Boost.VariantまたはC++17が使えるなら、std::variant<gray_image, rgba_image>
を返すラッパー関数を定義して、それをバインドすることができます。
std::variant<gray_image, rgba_image> read_image_wrapper(const std::string& fname)
{
if (is_grayscale(fname)){return read_image<gray_pixel>(fname);}
else if(is_rgbapixel(fname)){return read_image<rgba_pixel>(fname);}
else {throw std::runtime_error("invalid image format");}
}
mod.def("read_image", &read_image_wrapper, py::arg("filename") "read image file.");
しかし、BoostもC++17も使わなくて済む方法があります。image
がバインドされているのなら、それはpy::object
にキャストできます。なので、py::object
を返す関数にしてしまって、中でキャストして返せばいいのです。
py::object read_image_wrapper(const std::string& fname)
{
if (is_grayscale(fname)){return py::cast(read_image<gray_pixel>(fname));}
else if(is_rgbapixel(fname)){return py::cast(read_image<rgba_pixel>(fname));}
else {throw std::runtime_error("invalid image format");}
}
mod.def("read_image", &read_image_wrapper, py::arg("filename") "read image file.");
もう一段階楽をしましょう。Pythonにバインドするためだけに定義したので、どうせこのラッパー関数はこの場でしか使いません。ところで、C++にはその場で使う関数オブジェクトを定義する方法がありませんでしたっけ。
mod.def("read_image", [](const std::string& fname) -> py::object {
if (is_grayscale(fname)){return py::cast(read_image<gray_pixel>(fname));}
else if(is_rgba(fname)) {return py::cast(read_image<rgba_pixel>(fname));}
else {throw std::runtime_error("invalid image format");}
}, py::arg("filename") "read image file.");
簡単になりました。
まとめ
- pybind11でバインド済みのクラス・構造体は、
py::cast
でpy::object
にキャストできる - C++のレベルでは返す型が違っても、
py::object
にしてしまえば同じ型にできる - pybind11はラムダもバインドできるのでラッパーはその場で定義すれば良い
- pybind11がつよい(小並感)