はじめに
こちらの記事は、eeic (東京大学工学部電気電子・電子情報工学科) Advent Calendar 2022 の 11 日目の記事として書かれたものです。
pybind11 という Python のライブラリを初めて使ってみたので、その紹介を簡単にしようと思います。
pybind11 とは
Python に C++ の関数やクラスを組み込めるライブラリです。
Python3.6+ と C++ の標準ライブラリのみに依存しているため、余計な依存関係をないのがポイントです。
古いコンパイラを無視して C++11 で書かれたものに限定することによって、軽量なヘッダのみのライブラリとすることができたらしいです。
詳しくは 公式ドキュメント をご覧ください。チュートリアルも充実しています。
自作ライブラリの作成
pybind11_demo
というライブラリを作成してみました。
ソースコードは こちらの GitHub レポジトリ にあります。
ヘッダファイルを include
ディレクトリにまとめています。
src
ディレクトリに C++ の実装をまとめています。
integer
と structure
という2つのサブモジュールがあり、それぞれのサブモジュールの中に関数やクラスが実装されている、という形になっています。
ディレクトリ構造について、 Python の自作ライブラリを書く時みたいに、 __init__.py
にあたるものが、各(サブ)モジュール内の (サブ)モジュール名.cpp
になっています。
setup.py
や pyproject.toml
はライブラリ作成時に必要なものです。(詳細は省略します。)
pybind11_demo/
├── include/
│ ├── integer.h
│ ├── pybind11_demo.h
│ └── structure.h
├── src/
│ ├── integer/
│ │ ├── add_i16.cpp
│ │ ├── add_i32.cpp
│ │ ├── add_i64.cpp
│ │ └── integer.cpp
│ ├── structure/
│ │ ├── pet_private.cpp
│ │ ├── pet_public.cpp
│ │ ├── pet.cpp
│ │ └── structure.cpp
│ └── pybind11_demo.cpp
├── pyproject.toml
└── setup.py
※VSCode 上での開発に役立ちそうな話
VSCode 上で C/C++ の拡張機能を使いながら c++ のコードを書いていると、
#include <pybind11/pybind11.h>
のところに波線が引かれてしまうと思います。これは、VSCode 上からだと pybind11.h
がインクルードのパスの中に見つからないせいなので、VSCode の設定に書き加えてあげます。具体的には、settings.json
に以下の内容を追加してあげます。
{
...
// add this block
"C_Cpp.default.includePath": [
"hoge",
"fuga",
],
...
}
hoge
や fuga
の部分にあたる、追加するべきインクルードパスは、以下のコマンドで調べることができます。
python -m pybind11 --includes
挙動の確認
整数どうしの足し算
以下のように、整数どうしの足し算であっても、使用する型によって異なる関数を定義することができます。今回は、16 / 32 / 64 bit の符号付き整数を試してみます。
// 16bit 符号付き整数どうしの足し算
short add_i16(short i, short j) {
return i + j;
}
// 32bit 符号付き整数どうしの足し算
int add_i32(int i, int j) {
return i + j;
}
// 64bit 符号付き整数どうしの足し算
long long add_i64(long long i, long long j) {
return i + j;
}
これらの関数を Python から呼び出して使ってみましょう。
>>> add_i16(1, 2)
3
>>> add_i32(1, 2)
3
>>> add_i64(1, 2)
3
どれも正しい結果が返ってきています。
Python といえば(?)多倍長整数なので、その挙動も見てみましょう。
>>> add_i16(2**14, 2**14)
-32768
>>> add_i32(2**14, 2**14)
32768
>>> add_i64(2**14, 2**14)
32768
short
型は $[-2^{15}, 2^{15})$ の値しか取れないのですが、 $2^{14} + 2^{14} = 2^{15}$($010...0_{(2)} + 010...0_{(2)} = 100...0_{(2)}$)となってしまい、オーバーフローしてしまいます。(詳細が分からない方は 2の補数 とかで調べてみてください。)
さらに大きな整数で計算を試してみましょう。
>>> add_i16(2**30, 2**30)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: add_i16(): incompatible function arguments. The following argument types are supported:
1. (arg0: int, arg1: int) -> int
Invoked with: 1073741824, 1073741824
>>> add_i32(2**30, 2**30)
-2147483648
>>> add_i64(2**30, 2**30)
2147483648
short
型ではエラーが出るようになりました。エラーの内容では、「非互換な関数の引数($1073741824 = 2^{30}, 1073741824 = 2^{30}$)がある」と言われています。short
型の取りうる値以外の入力があると、エラーが出るみたいですね。
int
型では、先ほどの short
型と同じような原因で、間違った計算結果になってしまっています。
自作クラス
基本となる Pet
クラスに基づいて、以下のような少しずつ異なるクラスを定義します。(詳しくは ソースコード をご覧ください。)
- 通常のクラス(
Pet
) - Python 接続時に
name
メンバ変数の読み書き権限を与えたクラス(PetPublic
) - c++ のクラス内で
naem
を private なメンバ変数としたクラス(PetPrivate
)
class Pet {
public:
Pet(const std::string &name) : name(name) { }
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
std::string name;
};
各クラスに __repr__
を実装してあるので、それぞれ見ていきましょう。
>>> pet = Pet("hoge")
>>> pet_public = PetPublic("hoge")
>>> pet_private = PetPrivate("hoge")
>>> print(pet)
<pybind11_demo.structure.Pet named 'hoge'>
>>> print(pet_public)
<pybind11_demo.structure.PetPublic named 'hoge'>
>>> print(pet_private)
<pybind11_demo.structure.PetPrivate>
__repr__
を実装する際に、name
メンバ変数にアクセスすることでペットの名前も表示することを実現したかったのですが、PetPrivate
クラスでは name
メンバ変数が private なので、直接アクセスする形では使用することができません。
次に、Python 側から name
メンバ変数に他の名前を直接代入できるか見てみましょう。
>>> pet.name = "fuga"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'pybind11_demo.structure.Pet' object has no attribute 'name'
>>> pet_public.name = "fuga"
>>> pet_public.name
'fuga'
>>> pet_private.name = "fuga"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
Python 上での Pet
クラスには name
メンバ変数はないよと怒られてしまっています。以下のように、メンバ変数もちゃんと定義してあげないと、public なメンバ変数であっても、アクセスすることはできません。
.def_readwrite("name", &PetPublic::name)
また、PetPrivate
クラスにおいて name
は、getName
メンバ関数を利用して、以下のように read-only な property として定義されています。そのため、can't set attribute
というエラーが出されています。(メンバ変数とプロパティを並列に紹介してしまっていいのかは微妙ですが、機能の紹介程度に思っていただければです。)
.def_property_readonly("name", &PetPrivate::getName)
おわりに
pybind11 ライブラリに入門してみた話でした。C++ で実装した関数や構造体を Python で手軽に使いたいと思った時、こちらのライブラリを思い出してみてください。
余談
chatGPT 呼ばれる大規模言語モデルが公開されました。これに「pybind11に入門してみた話」というクエリを投げてみたところ、以下の出力が得られました。
pybind11は、C++とPythonを比較的簡単に統合するためのライブラリです。これにより、PythonプログラマーがC++コードを使用して高速なプログラミングを行えるようになります。また、C++プログラマーがPythonの拡張モジュールを作成するのにも便利です。
pybind11を使用するためには、まずpybind11をインストールする必要があります。これは通常、pipを使用して行うことができます。
pip install pybind11
インストールが完了したら、次に、C++のコードをPythonの拡張モジュールに変換するために必要なpybind11のヘッダーファイルをインクルードします。
#include <pybind11/pybind11.h>
次に、Pythonの拡張モジュールを作成するためのpybind11の関数を呼び出します。この関数には、拡張モジュールの名前とC++の関数を引数として渡します。
PYBIND11_MODULE(my_extension, m) { m.def("my_function", &my_function); }
これで、Pythonプログラマーは拡張モジュールをインポートし、C++の関数を呼び出すことができます。
import my_extension my_extension.my_function()
pybind11を使用することで、C++とPythonを統合することができます。これにより、Pythonプログラマーが高速なC++コードを使用したプログラミングを行うことができます。また、C++プログラマーはPythonの拡張モジュールを簡単に作成することができます。
やばいですね☆
簡単な記事程度なら、このAI使えば書けてしまいそうです......