Python を Rust と PyO3 で高速化する
Python は簡潔さと豊富なサードパーティパッケージが魅力ですが、重い計算処理やギガバイト単位のデータの解析、数百万件のオブジェクトのバリデーションになると、インタープリタの速度が低下します。これまでは、このような状況で開発者は手動で C API 拡張を記述する必要がありました。このプロセスは骨の折れる作業です:メモリリークのデバッグや手動での参照管理が開発の喜びをすぐに奪い去ります。
数年前に、より安全な道ができました。PyO3 ライブラリなら、厳格なメモリ安全性と明確な抽象化を保ちながら、Rust でネイティブモジュールを記述できます。
Pydantic v2 のバリデーター、orjson の JSON パーサー、Polars フレームワーク、OpenAI の Tiktoken トークナイザーを使ったことがあるなら、すでに PyO3 で作成されたコードを実行しています。
Rust と Python を連携させる理由
Python はビジネスロジックやプロトタイピングに優れています。Rust は максимальная скорость、低いリソース消費、並行処理が必要な場面で強みを発揮します。
PyO3 は2つの問題を解決します:
- Python アプリケーションのボトルネックを個別の Rust 関数や構造体にオフロードする。
- Rust バイナリに Python インタープリタを埋め込み、スクリプト実行を可能にする。
ほとんどの場合、最初のシナリオで使用されます。
モジュールを実際に書く方法
Rust コードを Python パッケージとしてビルドするには、作者たちは maturin ツールを推奨しています。このツールがコンパイルを処理し、wheel ファイルを作成します。
2つの数値を加算し、結果を文字列として返すシンプルなモジュールを作成してみましょう。
まず、仮想環境を設定します:
mkdir string_sum && cd string_sum
python -m venv .env
source .env/bin/activate
pip install maturin
maturin init --bindings pyo3
Rust 側のロジックを src/lib.rs ファイルに記述します:
use pyo3::prelude::*;
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
#[pymodule]
mod string_sum {
use super::*;
#[pymodule_export]
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
}
#[pyfunction] と #[pymodule] マクロは、Python の C API 用コードを自動的に生成します。開発者が PyObject 型を Rust 構造体に変換したり、その逆を行ったり、手動で変換する必要はありません。
ビルドを開始します:
maturin develop
これで、Python から直接関数を呼び出せます:
import string_sum
result = string_sum.sum_as_string(5, 20)
print(result) # '25'
本番環境で実際のパフォーマンスを測定したい場合は、最適化フラグでモジュールをビルドするだけです: maturin develop --release。
実際のユースケース
Pydantic V2 では、すべての型チェックとバリデーションロジックが個別の Rust パッケージ pydantic-core に書き直されました。パフォーマンスは劇的に向上しました。
Polars ライブラリは、大規模データセットの処理で Pandas に取って代わりつつあります。これは Rust での並列計算 덕택です。ユーザーは使い慣れた Python コードを書けますが、重い処理はコンパイル済みモジュールで行われます。
機械学習では、PyO3 は Hugging Face の tokenizers パッケージや OpenAI の tiktoken で使用されています。ニューラルネットワークに送信する前の高速なテキスト解析により、サービスのレイテンシを大幅に削減できます。
サポートツール
PyO3 の周りには便利なユーティリティのセットが形成されています:
maturinは setup.py の複雑な設定を記述する必要を排除します。rust-numpyはメモリ内のデータコピーなしで Rust と NumPy 配列をバインドします。pyo3-async-runtimesは asyncio と Tokio 非同期ランタイムを接続します。pythonizeは Serde を使用してデータ構造を変換します。
結論
プロジェクトにパフォーマンスのボトルネックがある場合、PyO3 は C や C++ に切り替えずに問題を解決するのに役立ちます。ここで主な難しさは Rust 自体の学習にあります。所有権の概念とデータ型の理解が必要ですが、ライブラリは CPython との定型的な作業を完全に処理してくれます。
関連プロジェクト