近期笔者迷上了 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 调用 PythonRust 做 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。

GIL

  • Python API 必须在 GIL 内。
  • CPU 密集纯 Rust 代码:py.allow_threads(|| expensive()) 释放 GIL。
  • 注意:释放后不得操作 PyObject。
  • 并发设计:

    • 将数据放入 Arc<Mutex<T>> 或无锁结构(如 crossbeam)并在 Python 层保留 handle。
    • 确认 #[pyclass] 内部字段线程安全,必要时 #[pyclass(unsendable)] 防止错误发送。

性能与跨边界开销

  • 每次 Python -> Rust 调用 ≈ 几百纳秒到几微秒(取决于参数类型解析)。
  • 优化策略:

    1. 减少调用频率,扩大单次处理数据量(批量 API)。
    2. 避免频繁构造 Python 对象;在 Rust 内聚合再一次性转换。
    3. 使用 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 -> Python pyarrow 使用 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。
  • 用法:

    1. Initialize runtime:pyo3_asyncio::tokio::init_multi_thread().
    2. Python 调用 async 函数 -> 返回 asyncio.Future
    3. 在 Rust async 中若需要 Python 对象操作,使用 Python::with_gil.
  • 注意:

    • 避免双 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 编译:可以用 crosszig 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。

建议:

  1. 确定 baseline(原始纯 Python)。
  2. 用真实数据规模(避免 L1 cache 人工偏好)。
  3. 分离:

    • 纯算法时间(Rust 内部 benchmark:cargo bench
    • Python 调用封装层时间。
  4. 统计指标:P50/P90/STD,至少运行 30 次。
  5. 使用 pyperfpytest-benchmark

10. 与其它“加速工具”对比

方案优点缺点适合
Cython语法接近 Python,成熟性能不如手写优化 Rust/C 时极限;类型声明冗余中等性能 + 快速改造
NumbaJIT 快速试验,高速数值动态特性限制,启动开销科学计算热点函数
C/C++ 扩展极致掌控内存安全隐患老项目 / 现有 C 库封装
Rust + PyO3安全 + 性能 + 现代生态学习曲线、构建初期复杂新增模块/中长线
Mojo (探索)仍不成熟生态风险/不稳定未来观察
HPy新一代稳定 ABI 尝试生态尚早长期兼容性目标

策略:已有 Python 数值代码快速加速→先 Numba;长期可维护→Rust;需要 Python 语法亲和→Cython。


11. 常见踩坑清单

  1. 持久保存 &PyAny 超出 GIL 生命周期 -> UB。
  2. 忘记 allow_threads 导致多线程 Rust 算法仍被 GIL 串行。
  3. Panic 未捕获 -> 直接 abort 进程。
  4. 频繁 Python <-> Rust 单元素往返 -> 性能下降。
  5. 未区分 debug / release:Rust debug 可能慢数倍。
  6. Windows 构建缺少 MSVC 组件。
  7. manylinux 环境使用 glibc 不兼容版本 -> 导致导入失败。
  8. 未设置 RUSTFLAGS="-C target-cpu=native"(若部署环境统一可开启更多指令)。
  9. 过度使用 unsafe 手写 FFI,忽略 PyO3 安全 abstractions。
  10. 忽视内存峰值: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)
    })
}

标签: none

添加新评论