PythonとRustを連携させてパフォーマンスを向上させる
Olivia Novak
Dev Intern · Leapcell

PythonとRustを連携させてパフォーマンスを向上させる
Pythonは、その可読性と広範なエコシステムで知られていますが、計算集約的なタスクではパフォーマンスのボトルネックに直面することがよくあります。データ処理、科学シミュレーション、リアルタイムアプリケーションのいずれであっても、グローバルインタープリタロック(GIL)とそのインタープリタ型は、速度を低下させる可能性があります。一方、Rustは比類のないパフォーマンス、メモリ安全性、スレッド並列性を提供し、Pythonコードのクリティカルセクションを最適化する理想的な候補となります。この記事では、特にPyO3とrust-cpythonという2つの著名なライブラリに焦点を当て、RustコードをPythonアプリケーションに効果的に統合する方法について説明します。Rustのパワーを活用することで、Python開発者はPythonが提供する利便性を犠牲にすることなく、より高速で信頼性の高いアプリケーションを構築できます。
コアコンセプト
PyO3とrust-cpythonの具体例に入る前に、このクロス言語統合を理解するために不可欠ないくつかのコアコンセプトを明確にしましょう。
- 外部関数インターフェース(FFI): FFIは、あるプログラミング言語で書かれたプログラムが、別のプログラミング言語で書かれたルーチンを呼び出したり、サービスを利用したりするメカニズムです。PyO3とrust-cpythonはどちらも、PythonとRust間の通信を可能にするために、PythonのFFIであるPython C APIに依存しています。
- Python C API: これはPythonインタープリタによって提供されるCレベルのAPIであり、C(およびFFI経由のRust)プログラムがPythonオブジェクトと対話したり、Pythonコードを実行したり、Pythonインタープリタを拡張したりすることを可能にします。
- モジュールと関数のエクスポート: RustコードをPythonから呼び出せるようにするには、Rust関数と構造体をPythonモジュールまたは呼び出し可能なオブジェクトとしてエクスポートする必要があります。これには、慎重な型マッピングとPythonのオブジェクトモデルへの準拠が含まれます。
- メモリ管理: FFIの重要な側面は、言語境界を越えてメモリを安全に管理することです。Rustの所有権システムはRust内でのメモリ安全性を保証しますが、Pythonと対話する際には、メモリリークやクラッシュを防ぐために、PythonのガベージコレクタとC APIの規則に留意する必要があります。
RustをPythonと統合する:PyO3とrust-cpython
PyO3とrust-cpythonはどちらもPythonからRustコードを呼び出すための強力なツールですが、それぞれアプローチと機能が若干異なります。
PyO3:ハイレベルでイディオマティックなアプローチ
PyO3は、RustでPythonモジュールを記述するためのハイレベルなバインディングを提供する、人気のあるアクティブにメンテナンスされているライブラリです。人間工学的でイディオマティックであることを目指しており、Rust開発者がPythonエコシステムと対話する際に快適に過ごせるようにしています。PyO3は、Pythonモジュール、クラス、関数を定義するためのRustライクなマクロとトレイトを提供し、Python C APIの多くの複雑さを抽象化します。
原則: PyO3はRustのマクロシステムを活用して、必要なC APIのボイラープレートコードを生成します。Rust関数と構造体を定義し、PyO3マクロで注釈を付けると、PyO3がPython型からRust型への、またはその逆の変換を処理します。
例: Pythonで公開するために、2つの数値の合計を計算する簡単なRust関数を作成してみましょう。
まず、Cargo.toml
をセットアップします。
[package] name = "my_rust_module" version = "0.1.0" edition = "2021" [lib] name = "my_rust_module" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.21", features = ["extension-module"] }
次に、src/lib.rs
にRustコードを記述します。
use pyo3::prelude::*; /// 2つの数値の合計を文字列としてフォーマットします。 #[pyfunction] fn sum_as_string(a: usize, b: usize) -> PyResult<String> { Ok((a + b).to_string()) } /// Rustで実装されたPythonモジュール。 #[pymodule] fn my_rust_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?) Ok(()) }
これをビルドするには、プロジェクトのルートで maturin develop
を実行します(pip install maturin
で maturin
をインストールする必要があります)。これにより、Rustコードがコンパイルされ、Pythonパッケージとしてインストールされます。
これで、Pythonで使用できます。
import my_rust_module result = my_rust_module.sum_as_string(5, 7) print(f"The sum is: {result}") # 出力: The sum is: 12 assert result == "12"
アプリケーションシナリオ: PyO3は、Rustでゼロから新しいPythonモジュールを作成したり、パフォーマンスが重要なループを最適化したり、既存のRustライブラリをPythonに統合したり、境界を越えて複雑なデータ構造を処理したりするのに最適です。その人間工学に基づいた設計は、単純な関数と複雑なクラス階層の両方に適しています。
rust-cpython:より低レベルで明示的なアプローチ
rust-cpython
は、Python C APIのより直接的で低レベルなラッパーを提供します。Pythonの内部オブジェクトモデルとC APIについての深い理解が必要ですが、対話のきめ細かな制御を提供します。PyO3よりも意見が少なく、生のPythonオブジェクトへのより近いアクセスを提供します。
原則: rust-cpython
は、PythonのC APIオブジェクト(例:PyObject
、PyDict
、PyList
)をミラーリングするRust型を公開します。参照カウントと型変換を手動で処理し、メモリ管理とオブジェクト対話の明示的な制御を提供します。
例: rust-cpython
を使用してsum_as_string
関数を再実装してみましょう。
まず、Cargo.toml
を更新します。
[package] name = "my_rust_module_cpython" version = "0.1.0" edition = "2021" [lib] name = "my_rust_module_cpython" crate-type = ["cdylib"] [dependencies] cpython = "0.7" # またはそれ以降の互換性のあるバージョン
次に、src/lib.rs
にRustコードを記述します。
extern crate cpython; use cpython::{PyResult, Python, PyModule, PyDict, PyObject, ToPyObject}; fn sum_as_string_cpython(py: Python, a: PyObject, b: PyObject) -> PyResult<String> { let a_int: usize = a.extract(py)?; let b_int: usize = b.extract(py)?; Ok((a_int + b_int).to_string()) } // モジュールをエクスポートするためにcpythonによって提供される特別なマクロ py_module_initializer!(my_rust_module_cpython, initmy_rust_module_cpython, PyInit_my_rust_module_cpython, |py, m| { m.add(py, "__doc__", "rust-cpythonでRustで実装されたPythonモジュール。")?; m.add_wrapped(py, "sum_as_string", sum_as_string_cpython)?; Ok(()) });
これをビルドするには、以前と同じように maturin develop
を使用します。
これで、Pythonで次のように使用できます。
import my_rust_module_cpython result = my_rust_module_cpython.sum_as_string(10, 20) print(f"The sum is: {result}") # 出力: The sum is: 30 assert result == "30"
アプリケーションシナリオ: rust-cpython
は、Pythonオブジェクトの詳細な制御が必要なシナリオ、おそらく非常に低レベルでのパフォーマンスが重要な対話のため、または内部Python構造との直接的なインターフェースのために適しています。Python C APIにすでに精通しており、Rustでより近いマッピングを望む開発者に好まれるかもしれません。
--- Pyo3 vs Rust-cpython 比較 ---
特徴 | PyO3 |
---|---|
使いやすさ | ハイレベル、人間工学的、Rustイディオマティック |
抽象化 | Python C APIのハイレベルな抽象化 |
マクロ | バインディングのためにマクロを多用 |
エラー処理 | Rust Result と PyErr |
型変換 | FromPyObject /ToPyObject でほぼ自動 |
コミュニティ/アクティビティ | 非常にアクティブ、モダン |
特徴 | rust-cpython |
---|---|
使いやすさ | 低レベル、より明示的、C API中心 |
抽象化 | 低抽象化、直接的なC APIラッパー |
マクロ | マクロへの依存度が低く、より手動 |
エラー処理 | PyResult と手動エラー発生 |
型変換 | extract /to_pyobject を使用した明示的な変換 |
コミュニティ/アクティビティ | アクティブ度は低いが、安定している |
結論
PyO3とrust-cpythonはどちらも、高性能なRustコードをPythonアプリケーションに統合するための効果的なパスを提供し、開発者がクリティカルなタスクのインタープリタオーバーヘッドを克服できるようにします。PyO3は、その人間工学に基づいた設計とハイレベルな抽象化により、生産的でイディオマティックなRustエクスペリエンスを求めるほとんどのユーザーにとって、最適な選択肢となっています。Rust-cpythonは、Python C APIのさらに深く掘り下げる必要がある人々に、よりきめ細かな制御を提供します。これらの強力なツールの力を活用することで、開発者は大幅なパフォーマンス向上のロックを解除し、Pythonアプリケーションが要求の厳しいワークロードでも堅牢で応答性の高い状態を維持できるようにします。