Interoperabilidade com Python: PyO3 e bindings
1. Introdução ao PyO3 e ecossistema de bindings Rust↔Python
PyO3 é a principal biblioteca para criar bindings entre Rust e Python, permitindo que código Rust seja chamado diretamente do Python com performance nativa. Diferentemente de abordagens como ctypes (que requer wrappers manuais) ou CFFI (que depende de interfaces C), PyO3 oferece integração profunda com o sistema de tipos de ambas as linguagens, gerenciamento automático de memória via contagem de referências do Python, e suporte a async/await.
O ecossistema inclui maturin (ferramenta de build e publicação), setuptools-rust (integração com setuptools tradicional) e suporte nativo a PyPI. Casos de uso típicos incluem aceleração de código Python computacionalmente intensivo, exposição de bibliotecas Rust seguras (com garantias de memória) e criação de extensões nativas para frameworks científicos como NumPy.
2. Configuração do ambiente e primeiro binding
Para começar, instale as ferramentas necessárias:
python -m venv .venv
source .venv/bin/activate
pip install maturin
Crie um novo projeto:
maturin init --bindings pyo3
Isso gera a estrutura:
meu_projeto/
├── Cargo.toml
├── pyproject.toml
├── src/
│ └── lib.rs
No src/lib.rs, implemente uma função simples:
use pyo3::prelude::*;
#[pyfunction]
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[pymodule]
fn meu_projeto(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add, m)?)?;
Ok(())
}
Compile e teste:
maturin develop
python -c "import meu_projeto; print(meu_projeto.add(3, 4))" # Saída: 7
3. Tipos, conversões e manipulação de dados
PyO3 realiza conversão automática entre tipos Rust e Python:
use pyo3::prelude::*;
use std::collections::HashMap;
#[pyfunction]
fn process_data(numbers: Vec<f64>, mapping: HashMap<String, bool>) -> PyResult<(f64, Vec<String>)> {
let sum: f64 = numbers.iter().sum();
let active_keys: Vec<String> = mapping
.into_iter()
.filter(|(_, v)| *v)
.map(|(k, _)| k)
.collect();
Ok((sum, active_keys))
}
Para strings, use &str ou String:
#[pyfunction]
fn greet(name: &str) -> String {
format!("Olá, {}!", name)
}
Coleções como Vec<T> mapeiam para listas Python, HashMap<K,V> para dicionários. Para manipulação mais fina, use PyList e PyDict:
#[pyfunction]
fn manipulate_list(py: Python, lst: &PyList) -> PyResult<()> {
lst.append(42)?;
Ok(())
}
4. Expondo structs e objetos complexos com #[pyclass]
Transforme structs Rust em classes Python completas:
#[pyclass]
struct Contador {
#[pyo3(get, set)]
valor: i64,
nome: String,
}
#[pymethods]
impl Contador {
#[new]
fn new(nome: String) -> Self {
Contador { valor: 0, nome }
}
fn incrementar(&mut self, passo: i64) {
self.valor += passo;
}
#[getter]
fn nome(&self) -> &str {
&self.nome
}
#[staticmethod]
fn from_valor_inicial(valor: i64) -> Self {
Contador { valor, nome: String::from("padrão") }
}
}
Uso em Python:
c = Contador("meu_contador")
c.incrementar(5)
print(c.valor) # 5
print(c.nome) # "meu_contador"
Para herança, use #[pyclass(subclass)]:
#[pyclass(subclass)]
struct Animal {
nome: String,
}
#[pymethods]
impl Animal {
#[new]
fn new(nome: String) -> Self {
Animal { nome }
}
}
5. Tratamento de erros e exceções entre as linguagens
Converta Result Rust em exceções Python:
use pyo3::exceptions::PyValueError;
#[pyfunction]
fn dividir(a: f64, b: f64) -> PyResult<f64> {
if b == 0.0 {
return Err(PyValueError::new_err("Divisão por zero não permitida"));
}
Ok(a / b)
}
Crie exceções personalizadas:
#[pyclass]
#[derive(Clone)]
struct MeuErro {
mensagem: String,
}
impl std::fmt::Display for MeuErro {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.mensagem)
}
}
impl std::error::Error for MeuErro {}
impl From<MeuErro> for PyErr {
fn from(err: MeuErro) -> PyErr {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(err.mensagem)
}
}
Para capturar panics Rust:
use std::panic::catch_unwind;
#[pyfunction]
fn operacao_arriscada() -> PyResult<i32> {
let resultado = catch_unwind(|| {
// código que pode panicar
42
});
match resultado {
Ok(valor) => Ok(valor),
Err(_) => Err(pyo3::exceptions::PyRuntimeError::new_err("Panic capturado")),
}
}
6. Gerenciamento de lifetime, GIL e concorrência
O token Python<'py> garante acesso ao GIL:
#[pyfunction]
fn operacao_lenta(py: Python) -> PyResult<()> {
// Libera o GIL para operações bloqueantes
py.allow_threads(|| {
// código Rust pesado aqui
std::thread::sleep(std::time::Duration::from_secs(2));
});
Ok(())
}
Para callbacks, cuidado com lifetimes:
#[pyclass]
struct CallbackHandler {
callback: Py<PyAny>,
}
#[pymethods]
impl CallbackHandler {
#[new]
fn new(callback: Py<PyAny>) -> Self {
CallbackHandler { callback }
}
fn executar(&self, py: Python) -> PyResult<()> {
self.callback.call0(py)?;
Ok(())
}
}
7. Empacotamento, distribuição e publicação
Configure Cargo.toml:
[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
E pyproject.toml:
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "meu_projeto"
requires-python = ">=3.8"
Gere wheels multiplataforma:
maturin build --release
maturin publish # requer login no PyPI
Testes integrados com pytest:
# tests/test_modulo.py
import meu_projeto
def test_add():
assert meu_projeto.add(2, 3) == 5
8. Boas práticas, profiling e limitações
Performance: Use PyO3 quando a computação for intensiva (loops, processamento de dados). Para overhead pequeno, Python puro pode ser mais produtivo.
Profiling:
import timeit
import meu_projeto
# Compare performance
python_time = timeit.timeit("sum(range(1000000))", number=100)
rust_time = timeit.timeit("meu_projeto.soma_rust(range(1000000))", number=100)
Limitações:
- Custo de conversão para tipos grandes (e.g., arrays NumPy complexos)
- Overhead do GIL em chamadas frequentes
- Dificuldade com tipos genéricos complexos
Alternativas: Para computação numérica, considere Cython ou Numba. rust-cpython é legado e não recomendado para novos projetos.
PyO3 representa o estado-da-arte para bindings Rust↔Python, combinando segurança de memória do Rust com a flexibilidade do ecossistema Python.
Referências
- Documentação oficial do PyO3 — Guia completo com exemplos, referência de API e melhores práticas para desenvolvimento de extensões Python em Rust
- Maturin: Build and publish Rust crates as Python packages — Ferramenta oficial para compilar e publicar wheels PyO3 no PyPI
- PyO3 GitHub Repository — Código fonte, issues, discussões e exemplos avançados da comunidade
- Using Rust to Supercharge Your Python Code — Palestra técnica sobre otimização de Python com PyO3 (PyCon US)
- Rust↔Python Interoperability Guide — Artigo da Red Hat cobrindo PyO3, maturin e casos de uso empresariais
- PyO3 Examples Repository — Coleção oficial de exemplos práticos incluindo integração com NumPy, async e web frameworks