Hoe Python te versnellen met Rust en PyO3
Python is prachtig door zijn beknoptheid en rijke selectie aan third-party packages, maar bij zware berekeningen, het parsen van gigabytes aan data of het valideren van miljoenen objecten, begint de interpreter te vertragen. Voorheen moesten ontwikkelaars in zulke situaties handmatig C API-extensies schrijven. Dit proces is arbeidsintensief: het debuggen van geheugenlekken en handmatig referentiemanagement elimineert snel alle ontwikkelingsplezier.
Een veiliger pad ontstond enkele jaren geleden. De PyO3-bibliotheek maakt het mogelijk om native modules in Rust te schrijven met strikte geheugenveiligheidsgaranties en duidelijke abstracties.
Als je de Pydantic v2-validator, de orjson JSON-parser, het Polars-framework of OpenAI's Tiktoken-tokenizer hebt gebruikt, heb je al code uitgevoerd die met PyO3 is gemaakt.
Waarom Rust en Python koppelen
Python blinkt uit in bedrijfslogica en snelle prototyping. Rust wint waar maximale snelheid, laag resourceverbruik en concurrency nodig zijn.
PyO3 lost twee problemen op:
- Het verplaatsen van Python-applicatieflessenhalsen naar aparte Rust-functies of -structuren.
- De Python-interpreter in een Rust-binary inbedden voor scriptuitvoering.
Meestal wordt het project gebruikt voor het eerste scenario.
Hoe schrijf je een module in de praktijk
Voor het bouwen van Rust-code naar een Python-package raden de auteurs het maturin-tool aan. Het verwerkt compilatie en maakt wheel-bestanden.
Laten we een eenvoudige module maken die twee getallen optelt en het resultaat als string retourneert.
Eerst stellen we de virtuele omgeving in:
mkdir string_sum && cd string_sum
python -m venv .env
source .env/bin/activate
pip install maturin
maturin init --bindings pyo3
De Rust-side logica in het src/lib.rs-bestand:
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(())
}
}
De #[pyfunction] en #[pymodule] macros genereren automatisch code voor Python's C API. Ontwikkelaars hoeven PyObject-types niet handmatig naar Rust-structuren te converteren en vice versa.
Laten we de build starten:
maturin develop
Nu kan de functie rechtstreeks vanuit Python worden aangeroepen:
import string_sum
result = string_sum.sum_as_string(5, 20)
print(result) # '25'
Als je de echte prestaties in productie wilt meten, bouw je de module simpelweg met de optimalisatie-vlag: maturin develop --release.
Praktische toepassingen
In Pydantic V2 is alle type-checking en validatielogica herschreven in Rust in een apart package pydantic-core. De prestaties zijn dramatisch verbeterd.
De Polars-bibliotheek verdringt Pandas bij het werken met grote datasets dankzij parallelle berekeningen in Rust. De gebruiker schrijft vertrouwde Python-code, terwijl het zware werk in de gecompileerde module plaatsvindt.
In machine learning wordt PyO3 gebruikt door Hugging Face in het tokenizers-package en door OpenAI in tiktoken. Snelle tekstparsing voordat deze naar het neurale netwerk wordt gestuurd, vermindert de servicelatentie aanzienlijk.
Ondersteunende tools
Rond PyO3 heeft zich een set nuttige utilities gevormd:
maturinelimineert de noodzaak om complexe configs voor setup.py te schrijven.rust-numpybindt NumPy-arrays met Rust zonder data in het geheugen te kopiëren.pyo3-async-runtimesverbindt asyncio met de Tokio async runtime.pythonizetransformeert datastructuren met Serde.
Conclusie
Als je project een prestatieflessenhals heeft, zal PyO3 helpen het probleem op te lossen zonder over te schakelen naar C of C++. De grootste moeilijkheid hier ligt in het leren van Rust zelf. Het begrijpen van ownership-concepten en datatypes is vereist, maar de bibliotheek handelt alle routinematige werk met CPython volledig af.
Gerelateerde projecten