从零到一 —— 使用 PyO3 将 Rust 函数编译为 Python 模块
1. 引言:消除隔阂,一步调用
在上一篇中,我们分别用 Python 和 Rust 写了两个独立的 HTTP 服务器。虽然对比了性能,但它们本质上是两个独立的程序。
在实际开发中,我们往往不想维护两套服务。我们希望的结果是:Python 负责主流程和业务逻辑,而在性能瓶颈处,直接调用 Rust 编写的函数,就像调用普通的 Python 库一样自然。
这就需要一个桥梁。PyO3 就是这个桥梁,它允许你将 Rust 代码编译成一个.so(Linux/Mac)或.pyd(Windows)的二进制模块。Python 通过import加载它,中间的转换开销极小。
今天,我们就把上一篇中的count_words函数,打包成一个名为rust_analyzer的 Python 扩展库。
2. 环境准备:Maturin 工具链
为了编译 Rust 生成的 Python 模块,我们需要特定的工具。maturin是 PyO3 官方推荐的工具,它极大地简化了配置过程。
2.1 创建虚拟环境
为了保持环境整洁,我们先创建一个 Python 虚拟环境:
# 创建项目文件夹 mkdir pyo3_demo && cd pyo3_demo # 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate2.2 安装 Maturin
pip install maturin3. 初始化 PyO3 项目
maturin提供了一个命令,可以快速生成符合 PyO3 规范的项目骨架。
# 在当前目录下初始化一个名为 rust_analyzer 的库 maturin init --bindings pyo3运行后,它会询问你选择哪种绑定方式,直接回车选择默认的pyo3即可。
生成的项目结构如下:
pyo3_demo/ ├── Cargo.toml # Rust 项目管理文件 ├── pyproject.toml # Python 构建配置文件 └── src/ └── lib.rs # Rust 源代码(主要编辑这里)4. 编写 Rust 代码:导出函数
打开src/lib.rs,清空默认内容,填入以下代码。这段代码的核心任务是:告诉 Rust 如何将函数暴露给 Python。
文件路径:src/lib.rs
/* * 使用 PyO3 编写 Python 扩展模块 * 目标:将上一篇的 Rust 词频统计逻辑暴露给 Python */ // 引入 PyO3 预导入模块,包含所有必要的宏和类型 use pyo3::prelude::*; use std::collections::HashMap; /// 核心逻辑:纯粹的 Rust 函数 /// 这个函数不关心 Python,只负责高效的词频计算 /// 输入: 文本字符串和需要返回的 Top N /// 输出: 元组数组 (单词, 次数) fn count_words_rust(text: &str, top_n: usize) -> Vec<(String, u64)> { let mut freq_map = HashMap::new(); // 使用 Rust 高效的迭代器进行分词和统计 text.split(|c: char| !c.is_alphanumeric()) .filter(|s| !s.is_empty()) .for_each(|word| { *freq_map.entry(word.to_lowercase()).or_insert(0) += 1; }); // 排序并截取前 N 名 let mut freq_vec: Vec<_> = freq_map.into_iter().collect(); freq_vec.sort_by(|a, b| b.1.cmp(&a.1)); freq_vec.truncate(top_n); freq_vec } /// Python 接口函数 /// #[pyfunction] 宏:标记此函数为 Python 可调用函数 /// 参数类型会自动转换:Python str -> &str, Python int -> usize /// 返回值会自动转换:Vec<(String, u64)> -> Python list of tuples #[pyfunction] fn count_words(text: &str, top_n: usize) -> PyResult<Vec<(String, u64)>> { // 调用 Rust 核心逻辑 let result = count_words_rust(text, top_n); // Ok(...) 是 PyO3 的标准返回方式,用于处理 Python 异常 Ok(result) } /// 定义 Python 模块 /// #[pymodule] 宏:定义模块名称为 rust_analyzer /// _py: Python 解释器的句柄(GIL Token) /// m: 模块对象本身 #[pymodule] fn rust_analyzer(_py: Python<'_>, m: &PyModule) -> PyResult<()> { // 将 count_words 函数注册到模块中 m.add_function(wrap_pyfunction!(count_words, m)?)?; Ok(()) }关键点解析(博客重点):
分离逻辑:
count_words_rust是纯逻辑,count_words是接口层。这样做的好处是 Rust 代码可以不依赖 Python 环境进行单元测试。#[pyfunction]:这是魔法发生的地方,它让 Rust 函数能被import进来。PyResult:Rust 的错误处理与 Python 的异常系统对接。
5. 编译与安装(开发模式)
这是最关键的一步。我们需要将 Rust 代码编译成二进制文件,并链接到当前的 Python 虚拟环境中。
在项目根目录(pyo3_demo/)下执行:
maturin develop发生了什么?
cargo开始编译 Rust 代码。生成一个二进制文件(如
rust_analyzer.cpython-310-x86_64-linux-gnu.so)。maturin将这个文件复制到当前虚拟环境的site-packages目录下。
看到类似📦 Built wheel for CPython 3.10和✏️ Installed rust_analyzer的字样,就意味着成功了!
6. 在 Python 中调用
现在,激动人心的时刻到了。我们可以在 Python 中像使用普通库一样使用刚才编译的 Rust 模块。
在项目根目录下创建一个测试文件test.py:
文件路径:test.py
# 直接导入我们刚刚用 Rust 编译的模块 import rust_analyzer def test_rust_function(): print("--- Testing Rust Module ---") # 准备测试数据 text = "Rust is fast and safe. Python is flexible and easy. Rust and Python together are powerful." top_n = 3 # 直接调用 Rust 函数 # PyO3 自动帮我们把 Python str 转成了 Rust &str # 并把 Rust 返回的 Vec<(String, u64)> 转成了 Python list result = rust_analyzer.count_words(text, top_n) print(f"Input Text: {text}") print(f"Top {top_n} words:") for word, count in result: print(f" {word}: {count}") if __name__ == "__main__": test_rust_function()运行测试:
python test.py预期输出:
--- Testing Rust Module --- Input Text: Rust is fast and safe. Python is flexible and easy. Rust and Python together are powerful. Top 3 words: rust: 2 python: 2 and: 27. 流程总结与 Mermaid 图示
整个流程可以简化为以下几个步骤:
8. 总结
至此,我们成功打通了 Rust 到 Python 的调用链路。
没有 HTTP 开销:函数调用发生在进程内部,速度极快。
开发体验流畅:
maturin develop支持增量编译,修改 Rust 代码后重新运行该命令即可更新模块。类型安全:Rust 的强类型系统在编译期拦截了大量错误,Python 端拿到的数据是安全的。
