Python with Rust
近期笔者迷上了 Python 和 Rust 的组合,因为笔者对 Python 更加熟悉,曾经用 Python 完成许多后端系统、爬虫、AI 算法以及使用了 PyQt5 的客户端程序,对于 Rust 的了解还尚处于一个比较短浅的地步。但是,笔者也有着一些 C++ 经验,因此可以发现许多 Rust 的设计是为了避免 C++ 中一些坑的。
笔者认为,Python + Rust 的最佳姿势是:让 Python 保持“胶水 + 表达 + 快速迭代”,让 Rust 承担“稳定 + 高性能 + 并发安全”。避免陷入“全都用 Rust 重写”的过度工程;从可衡量的瓶颈出发,采用批处理、zero-copy、释放 GIL 等策略逐步获得可持续收益。在工具方面 PyO3 + maturin 已足够生产成熟,配合 tokio 与 Arrow/NumPy 等生态可以覆盖绝大多数性能与数据密集场景。
1. 为什么要把 Rust 引入 Python 技术栈
动机分类:
- 性能:热点循环 / 解析 / 序列化 / 压缩 / 加密 / 图算法 / 向量计算。
- 并发:利用 Rust 无数据竞争 + 释放 GIL 做多线程 IO/CPU。
- 内存安全:替换 C/C++ 扩展的风险区域。
- 可部署性:构建为单个 wheels,提供稳定 ABI(abi3),减少平台差异。
- 类型严谨:Rust 编译期保证 invariants,减少线上不可预期异常。
- 生态叠加:使用成熟 crates(serde、arrow、parquet、regex、tantivy、polars-core 等)。
不要误解:
- Rust 并不“自动让所有 Python 代码变快”,只有把热点用 Rust 重写或减少 Python 调用频率才有显著收益。
- FFI 过度切分反而变慢:跨边界有固定成本(几十纳秒到数微秒级,视场景)。
2. 几种常见的集成模式对比
模式 | 描述 | 优点 | 缺点 | 适用 |
---|---|---|---|---|
PyO3 原生扩展 | 编写 Rust crate 暴露 Python 模块 | 生态成熟、语法糖多 | 学习曲线 | 绝大多数新项目 |
rust-cpython | 老牌绑定 | 稳定 | 社区热度下降 | 维护历史项目 |
cffi/ctypes + Rust #[no_mangle] | 手写 C ABI | 简单入门 | 手工繁琐/易错 | 小函数/实验 |
进程隔离(子进程、gRPC) | Rust 独立服务 | 崩溃隔离、可多语言复用 | RPC 开销 | 重服务化架构 |
Arrow / IPC / 内存映射 | 共享列式内存 | 高通量零拷贝 | 协议理解成本 | 大数据流水线 |
WASM + Python (Pyodide/wasmtime) | 沙箱 | 安全、可嵌入 | 生态不全 | 特殊安全场景 |
Embedding Python in Rust | 主进程 Rust 调用 Python | Rust 做 orchestrator | 双运行时复杂 | Rust 主导系统 |
3. 主流方案与工具详解
PyO3
- 功能:用 Rust 写 Python 扩展;通过宏
#[pyfunction]
,#[pymodule]
,#[pyclass]
。 - 支持:类型转换(FromPyObject/IntoPy)、异常映射、Buffer/NB 协议、GIL 安全封装。
- 特点:宏式 ergonomics + 零成本 wrapper(大致接近手写 C-API 性能)。
maturin
- 一站式构建/发布工具:
maturin develop
,maturin build
,maturin publish
- 支持:PEP 517、生成 wheels、manylinux/aarch64/macos/universal2、abi3。
推荐结构:
project/ pyproject.toml Cargo.toml src/lib.rs yourpkg/__init__.py
构建 Python 逻辑混合:
- Python 层 orchestrate / 参数处理
- Rust 层核心算法
rust-cpython
- 现多用于遗留项目;新项目尽量用 PyO3。
其它:
- pyo3-asyncio:桥接 tokio/async-std 与 Python asyncio。
- pyo3-log:将 Rust log 转 Python logging。
- setuptools-rust:另一种构建方式(逐渐被 maturin 吸收场景)。
4. 什么时候该用 / 不该用 Rust
应该用:
- 重度 CPU 热点(>10% 总 CPU)且算法逻辑稳定。
- 需要多线程 CPU 并行(GIL 成瓶颈)。
- 内存安全/安全审计要求高(替换手写 C/C++ 扩展)。
- 要做高效数据桥接(Parquet、Arrow、压缩编解码)。
不适合:
- 业务规则频繁变化逻辑(Rust 重构成本较高)。
- 纯 I/O 轻业务 + 已有成熟 Python async 框架可满足。
- 小团队初期快速迭代 MVP。
- 性能瓶颈在外部服务(数据库、网络)而非 Python 解释器。
5. PyO3 实战核心知识点
基础结构示例
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
#[pyfunction]
fn sum_positive(xs: Vec<i64>) -> PyResult<i64> {
if xs.iter().any(|&v| v < 0) {
return Err(PyValueError::new_err("contains negative"));
}
Ok(xs.iter().sum())
}
#[pyclass]
struct Counter {
value: i64
}
#[pymethods]
impl Counter {
#[new]
fn new() -> Self { Self { value: 0 } }
fn inc(&mut self, n: i64) {
self.value += n;
}
#[getter]
fn value(&self) -> i64 { self.value }
}
#[pymodule]
fn fastmod(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_positive, m)?)?;
m.add_class::<Counter>()?;
Ok(())
}
类型转换
- Rust -> Python:实现
IntoPy<PyObject>
或使用Py::new
。 - Python -> Rust:实现
FromPyObject
(常用派生#[derive(FromPyObject)]
)。 - Zero-copy 与缓冲区:使用
pyo3::types::PyBytes
,pyo3::types::PyMemoryView
;NumPy 用numpy
crate 的PyArray<T, D>
(需要启用 feature)。
错误处理
- 返回
PyResult<T>
;将 Rust error map 到 Python 异常。 - 自定义异常:
create_exception!(mycrate, MyError, pyo3::exceptions::PyException);
避免 panic 传播:默认 panic 会 unwinding(可能导致 UB),建议:
- 在 Cargo.toml:
[profile.release] panic = "abort"
或 - 外围
std::panic::catch_unwind
包裹,转 PyErr。
- 在 Cargo.toml:
GIL
- Python API 必须在 GIL 内。
- CPU 密集纯 Rust 代码:
py.allow_threads(|| expensive())
释放 GIL。 - 注意:释放后不得操作 PyObject。
并发设计:
- 将数据放入
Arc<Mutex<T>>
或无锁结构(如 crossbeam)并在 Python 层保留 handle。 - 确认
#[pyclass]
内部字段线程安全,必要时#[pyclass(unsendable)]
防止错误发送。
- 将数据放入
性能与跨边界开销
- 每次 Python -> Rust 调用 ≈ 几百纳秒到几微秒(取决于参数类型解析)。
优化策略:
- 减少调用频率,扩大单次处理数据量(批量 API)。
- 避免频繁构造 Python 对象;在 Rust 内聚合再一次性转换。
- 使用
PyBuffer
/PyArray
直接读取底层内存。
内存与生命周期
- PyO3 通过引用计数管理。
- Rust 持有 Python 对象:使用
Py<PyAny>
(GIL 无关),在需要时as_ref(py)
。 - 避免悬垂:不要存
&PyAny
超出 GIL 生命周期。 - Zero-copy 时需保证底层 buffer 未被释放(遵循 Python 的引用计数即可)。
6. 加速数值/数据处理常见模式
NumPy + Rust
numpy
crate 提供PyArray1<f64>
等。转换:
use numpy::{PyArray1}; #[pyfunction] fn scale(py: Python<'_>, arr: &PyArray1<f64>, factor: f64) -> Py<PyArray1<f64>> { let slice = unsafe { arr.as_slice().unwrap() }; let out = PyArray1::<f64>::from_iter(py, slice.iter().map(|v| v*factor)); out.to_owned() }
Apache Arrow / Polars
- 共享列式数据,减少序列化。
- 可通过
arrow
crate 构建RecordBatch
-> Pythonpyarrow
使用ffi
导出。 - Polars 在大量场景已内置 Rust 内核,可直接使用(若需求类似,可先评估是否“重复造轮子”)。
Zero-Copy Bytes
- 对文本/JSON 解析后再 Python 使用:Rust 产出
PyBytes
,or memoryview。 - 可通过
serde_json
+ 自定义结构,再一次性转 Python dict/list(注意 cost)。
SIMD
- 使用
packed_simd2
/std::simd
(nightly / stable 进展) 进行向量化;在 Python 层只暴露批量操作。
7. 异步生态:tokio + pyo3-asyncio
- 场景:Rust 内部发起高并发 IO(HTTP、数据库、消息队列) + Python 提供 API。
用法:
- Initialize runtime:
pyo3_asyncio::tokio::init_multi_thread()
. - Python 调用 async 函数 -> 返回
asyncio.Future
。 - 在 Rust async 中若需要 Python 对象操作,使用
Python::with_gil
.
- Initialize runtime:
注意:
- 避免双 runtime 死锁;只保留一个 tokio 全局 runtime。
- Block Python event loop 是常见错误,应把阻塞放到
spawn_blocking
+ allow_threads。
8. 构建与发布(Wheels、abi3、多平台)
pyproject.toml 示例
[build-system]
requires = ["maturin>=1.6,<2"]
build-backend = "maturin"
[project]
name = "fastmod"
version = "0.1.0"
requires-python = ">=3.8"
classifiers = ["Programming Language :: Python :: 3"]
[tool.maturin]
# 如果使用 abi3:
# bindings = "pyo3"
# python-source = "python"
# compatibility = "abi3"
多平台要点
- Linux manylinux:
maturin build --release --manylinux 2014
- Mac universal2:
--universal2
- Windows:MSVC toolchain,注意 CRT。
- Cross 编译:可以用
cross
或zig cc
辅助(有时需关闭 LTO)。
abi3
- 优点:一个 wheel 覆盖多个 Python 次版本(>= 指定最小)。
- 限制:不能使用尚未稳定的 CPython API;启用:Cargo feature
abi3-py38
。 - 是否启用判断:如果你不依赖特定新 API,且想减少 wheel 数量 -> 用。
CI
- GitHub Actions 矩阵:os * python-version
- Cache: cargo + pip。
- 生成并上传:
maturin publish -u __token__ -p $PYPI_API_TOKEN
.
9. Benchmark 正确认知与策略
常见误区:
- 用微小循环 N 次调用 Rust 函数 -> 结果不佳(FFI overhead 主导)。
- 在 debug build 下测试 -> Rust 优势未体现。
- 没禁用 Python 层 logging/动态检查导致 noise。
建议:
- 确定 baseline(原始纯 Python)。
- 用真实数据规模(避免 L1 cache 人工偏好)。
分离:
- 纯算法时间(Rust 内部 benchmark:
cargo bench
) - Python 调用封装层时间。
- 纯算法时间(Rust 内部 benchmark:
- 统计指标:P50/P90/STD,至少运行 30 次。
- 使用
pyperf
或pytest-benchmark
。
10. 与其它“加速工具”对比
方案 | 优点 | 缺点 | 适合 |
---|---|---|---|
Cython | 语法接近 Python,成熟 | 性能不如手写优化 Rust/C 时极限;类型声明冗余 | 中等性能 + 快速改造 |
Numba | JIT 快速试验,高速数值 | 动态特性限制,启动开销 | 科学计算热点函数 |
C/C++ 扩展 | 极致掌控 | 内存安全隐患 | 老项目 / 现有 C 库封装 |
Rust + PyO3 | 安全 + 性能 + 现代生态 | 学习曲线、构建初期复杂 | 新增模块/中长线 |
Mojo (探索) | 仍不成熟生态 | 风险/不稳定 | 未来观察 |
HPy | 新一代稳定 ABI 尝试 | 生态尚早 | 长期兼容性目标 |
策略:已有 Python 数值代码快速加速→先 Numba;长期可维护→Rust;需要 Python 语法亲和→Cython。
11. 常见踩坑清单
- 持久保存
&PyAny
超出 GIL 生命周期 -> UB。 - 忘记
allow_threads
导致多线程 Rust 算法仍被 GIL 串行。 - Panic 未捕获 -> 直接 abort 进程。
- 频繁 Python <-> Rust 单元素往返 -> 性能下降。
- 未区分 debug / release:Rust debug 可能慢数倍。
- Windows 构建缺少 MSVC 组件。
- manylinux 环境使用 glibc 不兼容版本 -> 导致导入失败。
- 未设置
RUSTFLAGS="-C target-cpu=native"
(若部署环境统一可开启更多指令)。 - 过度使用
unsafe
手写 FFI,忽略 PyO3 安全 abstractions。 - 忽视内存峰值:Rust 批量分配 -> Python 层感知延后(无 GC 调整)。
12. 一些实践中的代码样例
批量处理,减少边界成本
#[pyfunction]
fn batch_norm(xs: Vec<f32>) -> PyResult<Vec<f32>> {
if xs.is_empty() { return Ok(vec![]); }
let mean = xs.iter().sum::<f32>() / xs.len() as f32;
let var = xs.iter().map(|v| (v-mean)*(v-mean)).sum::<f32>() / xs.len() as f32;
let std = var.sqrt();
Ok(xs.into_iter().map(|v| (v-mean)/std).collect())
}
释放 GIL
#[pyfunction]
fn heavy(py: Python<'_>, n: u64) -> PyResult<u64> {
let result = py.allow_threads(|| {
(0..n).fold(0u64, |acc, v| acc.wrapping_add(v))
});
Ok(result)
}
自定义异常
use pyo3::{create_exception, exceptions::PyException};
create_exception!(fastmod, DataError, PyException);
#[pyfunction]
fn parse_positive(x: i64) -> PyResult<i64> {
if x < 0 {
Err(DataError::new_err("expected positive"))
} else { Ok(x) }
}
tokio async 桥接
#[pyfunction]
fn fetch_url(py: Python<'_>, url: String) -> PyResult<&PyAny> {
pyo3_asyncio::tokio::future_into_py(py, async move {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
})
}