在Python中运行Rust库(使用PyO3)
摘要
本文解释如何使用PyO3将Rust代码集成到Python中,通过构建一个用Rust编写的JSON解析器并将其作为Python包暴露来演示,涵盖设置步骤、类型转换和性能比较。
暂无内容
查看缓存全文
缓存时间: 2026/09/13 17:39
# 库如何在Python中运行Rust(使用PyO3)
来源:https://belderbos.dev/blog/how-libraries-run-rust-inside-python/
每次你使用Pydantic v2——大多数Python应用依赖的数据验证库——验证数据时,背后都有一个Rust扩展在工作。其核心组件`pydantic-core`正是基于PyO3构建的,也就是本文要介绍的同款工具链。本文将构建一个精简版的类似桥梁,小到可以一次读完:一个用Rust编写的JSON解析器,将其暴露给Python,以便你可以像使用其他包一样导入它。
最后一步,即将Rust结果转换为Python对象的过程,是在你移植任何代码前必须理解的:对于像解析器这样的应用,这一步的开销可能超过解析本身。
## 从Rust到导入的四个步骤
将Rust代码引入Python需要四个步骤:
1. 编写一个普通的Rust模块。
2. 使用PyO3宏为其添加注解。
3. 让maturin (https://github.com/PyO3/maturin) 编译并安装它。
4. 导入生成的结果。
**在Python中使用Rust:编写Rust模块,用PyO3宏注解,使用maturin构建,导入共享库**
`#[pyfunction]` 和 `#[pymodule]` 是两个负责连接的Rust宏。Rust的属性宏类似于Python的装饰器:它会重写其所在的函数,在这里就是添加让Python能够调用该函数的胶水代码,并处理边界处的类型转换和引用计数。
然后,Maturin会将crate编译为共享库(`.so`、`.dylib`、`.dll`),并将其放入你的虚拟环境中,这样`import`就能直接生效。
我曾在《如何使用PyO3和Maturin在Python中运行Rust》(https://rsbit.es/rust-in-python-pyo3-maturin/)中详细介绍过整个设置过程,从`cargo new`到第一次导入。第一个教程返回一个数字。本文将在此基础上继续,因为有趣的部分始于你返回一个*结构体*而非标量的时候。
## 解析器首先产生一个Rust值
这个解析器返回的结构是一个JSON树,这也是后续示例的核心。在我们的Python转Rust训练营(https://scriptertorust.com/)中,学生将花六周时间用Rust从头编写一个JSON解析器,手工实现分词器和递归下降解析器(不使用serde),然后通过PyO3将其暴露给Python。
Josh的版本在实际测试用例上击败了CPython的C json模块;Jochen的版本运行速度比Python版本快了3.5倍。这个[公开参考实现](https://github.com/bbelderbos/json_parser),即学生使用的干净版本代码,就是我将要讲解的代码。
解析器产生一个普通的Rust枚举。Rust枚举可以持有多种形状之一,每个变体都可以携带数据,因此它能清晰地映射JSON树:
```rust
pub enum JsonValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}
```
这棵树完全存在于Rust中。Python永远不会直接看到它。PyO3层是一个在其之上的薄适配器。
## 暴露一个函数
将一个函数暴露给Python只需要两行:
```rust
#[pyfunction]
fn parse_json<'py>(py: Python<'py>, input: &str) -> PyResult<&'py Bound<'py, PyAny>> {
parse(input)?.into_pyobject(py)
}
```
对于Python读者来说,函数签名是最有趣的部分:
- `py: Python<'py>` 是一个代表访问Python解释器的令牌,是你传递给需要访问Python对象的PyO3 API的参数。在传统的Python构建中,这种访问与持有GIL(全局解释器锁)相关联。PyO3将其交给你,你在需要接触Python对象的任何地方将其传递下去。
- `Bound<'py, PyAny>` 是对任意类型Python对象的句柄,是你所认为的`PyObject`的Rust侧表示。
- `PyResult` 是 `Result`:返回值,或者返回一个PyO3作为Python异常抛出的错误。
- `?` 运算符传播该错误。如果`parse`失败,函数会提前返回,Python会看到一个异常;否则,它将解包`JsonValue`并继续执行。
所以`parse(input)?`完成了实际工作,而`.into_pyobject(py)`构建了调用者请求的Python对象。最后一个调用是开销所在:它必须为树中的节点创建Python对象,在一个大型文档上,这可能会累积成比解析本身更多的工作。
## 返回的过程才是昂贵的部分
这就是为什么该转换不是免费的。
`.into_pyobject`会遍历整个`JsonValue`树,并将其重建为原生的Python对象:每个对象对应一个字典,每个数组对应一个列表,每个叶子节点对应一个浮点数或字符串。你通过实现`IntoPyObject` trait来提供这种转换,PyO3调用它将Rust值转换为Python值:
```rust
impl<'py> IntoPyObject<'py> for JsonValue {
fn into_pyobject(self, py: Python<'py>) -> Result<Bound<'py, PyAny>, PyErr> {
match self {
JsonValue::Null => Ok(py.None().into_bound(py)),
JsonValue::Number(n) => Ok(n.into_pyobject(py)?.to_owned().into_any()),
JsonValue::Object(obj) => {
let py_dict = PyDict::new(py);
for (k, v) in obj {
py_dict.set_item(k, v.into_pyobject(py)?)?; // 递归调用
}
Ok(py_dict.into_any())
}
// ...处理数组、字符串、布尔值
}
}
}
```
一个包含10万个值的文档意味着在边界处会创建大约10万个Python对象,这发生在解析完全完成之后。在一个大型文档上,这个物化循环(而非解析)可能主导端到端的处理时间。
## 错误以相同的方式跨越边界
返回值并不是唯一需要转换的东西。解析失败是一个具有类型的Rust错误,而Python想要的是一个异常。一个`From`实现(Rust用于将一种类型转换为另一种类型的trait)使得`?`能够完成这项工作:
```rust
impl From<JsonError> for PyErr {
fn from(err: JsonError) -> PyErr {
match err {
JsonError::UnterminatedString { position } => PyValueError::new_err(
format!("Unterminated string starting at position {position}")
),
// ...为每个错误变体编写一个分支,保留位置信息
}
}
}
```
现在,格式错误的输入会引发一个`ValueError`,并携带解析中断的偏移量。文件读取路径自动获得相同处理:`std::io::Error`已经可以转换为匹配的Python异常,因此找不到文件路径会引发`FileNotFoundError`。调用者获得了Python的语义,而Rust层并没有泄漏出去。
## 这对你自己的移植意味着什么
如果你要移植的Rust函数返回一个标量,那么直接移植然后继续即可。边界通常足够小,可以忽略。
如果它返回一个大型结构体,转换就是你的实际开销,并且在解析器本身速度提升后,这是下一步需要优化的地方。
预分配`PyDict`在边缘情况下可能有所帮助,但更大的收益是架构上的:如果调用者不会访问所有内容,就不要物化整棵树。返回一个惰性的、由Rust支持的视图,并按需构建Python对象。
所以,当你使用PyO3时,要对边界进行性能分析,而不仅仅是分析算法。让Rust运行得快是简单的一半。你在“出去”路上构建的、从Rust值到Python对象的这趟旅程,才是决定移植是否值得的一半。
*正在学习Rust?我共同运营一个为期6周的Python转Rust训练营(https://scriptertorust.com/),在那里你将构建一个高性能的JSON解析器并实现PyO3绑定。*
相似文章
Rust for CPython(Python Language Summit 2026)
在2026年Python Language Summit上,David Hewitt介绍了Rust for CPython项目的路线图、成功标准和时间表,提议从Python 3.16开始提供可选的Rust支持,并回应了关于平台支持和核心开发者专业能力方面的疑虑。
Protobuf-py:专为Python打造的Protocol Buffers库,毫不妥协
Buf 宣布推出 protobuf-py,一个从头编写的全新 Python Protocol Buffers 库,通过了完整的兼容性测试套件,生成可读的类型化 Python 代码,并提供可选的 Rust 加速以提高性能。
Python 还是 Rust?使用本地大语言模型编程
一篇讨论帖,询问在使用本地大语言模型辅助编程时,Rust 和 Python 哪个更好,并探讨 Rust 的编译器反馈是否有助于弥补在消费级硬件上运行的小型模型能力较弱的不足。
更优的电池
本文探讨了编程语言社区的社会架构如何影响标准库的设计与质量,并比较了Python、Go和Rust的方法。
超越 WASI:BrowserPod 3.0 让任何 Rust 应用在浏览器中运行
BrowserPod 3.0 已发布,支持在浏览器中执行任何 Rust 应用并提供完整的 Linux 系统调用支持,同时修复了 Node.js 问题并集成了 Python 初步支持,且无需修改代码。