Jak przyspieszyć Pythona za pomocą Rust i PyO3
Python zachwyca zwięzłością i bogatym wyborem pakietów zewnętrznych, ale gdy przychodzi do intensywnych obliczeń, parsowania gigabajtów danych czy walidacji milionów obiektów, interpreter zaczyna zwalniać. Wcześniej w takich sytuacjach programiści musieli ręcznie pisać rozszerzenia C API. Ten proces jest pracochłonny: debugowanie wycieków pamięci i ręczne zarządzanie referencjami szybko pozbywają się całej radości z programowania.
Kilka lat temu pojawiła się bezpieczniejsza ścieżka. Biblioteka PyO3 umożliwia pisanie natywnych modułów w Rust, zachowując rygorystyczne gwarancje bezpieczeństwa pamięci i przejrzyste abstrakcje.
Jeśli korzystałeś z walidatora Pydantic v2, parsera JSON orjson, frameworka Polars lub tokenizera Tiktoken od OpenAI, to już uruchamiałeś kod stworzony z PyO3.
Dlaczego łączyć Rust i Python
Python wyróżnia się w logice biznesowej i szybkim prototypowaniu. Rust wygrywa tam, gdzie potrzebna jest maksymalna szybkość, niskie zużycie zasobów i współbieżność.
PyO3 rozwiązuje dwa problemy:
- Odciążanie wąskich gardeł aplikacji Python do oddzielnych funkcji lub struktur Rust.
- Osadzanie interpretera Python w pliku binarnym Rust na potrzeby wykonywania skryptów.
Najczęściej projekt jest wykorzystywany w pierwszym scenariuszu.
Jak napisać moduł w praktyce
Do budowania kodu Rust w pakiet Python autorzy zalecają narzędzie maturin. Obsługuje ono kompilację i tworzy pliki wheel.
Stwórzmy prosty moduł, który dodaje dwie liczby i zwraca wynik jako ciąg znaków.
Najpierw skonfigurujmy środowisko wirtualne:
mkdir string_sum && cd string_sum
python -m venv .env
source .env/bin/activate
pip install maturin
maturin init --bindings pyo3
Logika po stronie Rust w pliku 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(())
}
}
Makra #[pyfunction] i #[pymodule] automatycznie generują kod dla C API Pythona. Programiści nie muszą ręcznie konwertować typów PyObject na struktury Rust i z powrotem.
Rozpocznijmy budowanie:
maturin develop
Teraz funkcję można wywoływać bezpośrednio z Pythona:
import string_sum
result = string_sum.sum_as_string(5, 20)
print(result) # '25'
Jeśli chcesz zmierzyć rzeczywistą wydajność w produkcji, po prostu zbuduj moduł z flagą optymalizacji: maturin develop --release.
Przykłady zastosowań w rzeczywistych projektach
W Pydantic V2 cała logika sprawdzania typów i walidacji została przepisana w Rust w osobnym pakiecie pydantic-core. Wydajność wzrosła dramatycznie.
Biblioteka Polars wypiera Pandas przy pracy z dużymi zbiorami danych dzięki równoległym obliczeniom w Rust. Użytkownik pisze znajomy kod Python, podczas gdy ciężka praca odbywa się w skompilowanym module.
W uczeniu maszynowym PyO3 jest wykorzystywany przez Hugging Face w pakiecie tokenizers oraz przez OpenAI w tiktoken. Szybkie parsowanie tekstu przed wysłaniem do sieci neuronowej znacząco redukuje opóźnienia usługi.
Narzędzia wspierające
Wokół PyO3 ukształtował się zestaw użytecznych narzędzi:
maturineliminuje potrzebę pisania złożonych konfiguracji dla setup.py.rust-numpywiąże tablice NumPy z Rust bez kopiowania danych w pamięci.pyo3-async-runtimesłączy asyncio z asynchronicznym środowiskiem wykonawczym Tokio.pythonizetransformuje struktury danych za pomocą Serde.
Podsumowanie
Jeśli Twój projekt ma wąskie gardło wydajności, PyO3 pomoże rozwiązać problem bez przełączania się na C lub C++. Główna trudność tutaj tkwi w nauce samego Rusta. Zrozumienie koncepcji własności i typów danych będzie wymagane, ale biblioteka w pełni obsługuje rutynową pracę z CPython.
Powiązane projekty