数据类型转换(上):基本类型在 Rust 与 Python 间的双向传递
说实话,跨语言调用时最让人头疼的,往往不是业务逻辑,而是数据类型怎么对齐。我刚开始做 Rust 和 Python 混合编程时,就踩过不少坑——明明 Python 那边传过来一个整数,Rust 这边死活不认。说白了,两套类型系统各有各的脾气,得摸透了才能让它们好好配合。
这一章,咱们就专门聊聊基本类型:int、float、bool、str。别看它们基础,实际项目中 80% 的数据传递都离不开它们。我会结合 PyO3 这个工具,带你走一遍双向转换的完整流程。
1. 整体架构:数据是怎么跨过语言边界的?
先看一张图,帮你建立整体认知。我习惯把跨语言调用想象成「两个国家之间的海关」——数据要过境,必须经过检查、打包、再拆包的过程。
你看,Python 那边是动态类型,Rust 这边是静态类型。PyO3 就像海关检查员,负责把 Python 的 PyAny 对象「拆包」成 Rust 的具体类型,反过来把 Rust 的值「打包」成 Python 对象。
2. 整数类型:精度和范围是第一道坎
先看整数。Python 的 int 是任意精度的,想多大就多大。Rust 的整数类型可就多了:i32、i64、u32、u64……每个都有固定范围。
举个例子,Python 里 2**100 轻轻松松,但你要把它传给 Rust 的 i32,那肯定炸锅。为什么会这样?因为 i32 最大只能表示 2,147,483,647。
2.1 Python → Rust:用 .extract() 提取
use pyo3::prelude::*;
#[pyfunction]
fn add_one(x: i32) -> i32 {
x + 1
}
#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add_one, m)?)?;
Ok(())
}
这段代码里,x: i32 就是告诉 PyO3:「请把 Python 传进来的值转成 i32」。如果 Python 那边传了个超大整数,PyO3 会直接抛 OverflowError。嗯,这里要注意:不是所有 Python int 都能安全落地。
numpy.int64 类型的值,表面上看是整数,但 PyO3 的自动转换没处理这个类型。结果程序跑着跑着就 panic 了。后来我加了一层手动检查,用 obj.extract::<i64>() 之前先判断类型,才彻底解决。
2.2 Rust → Python:用 .into_py() 包装
use pyo3::prelude::*;
#[pyfunction]
fn get_large_number() -> PyResult<i64> {
Ok(1_000_000_000_000) // 1万亿
}
Rust 的 i64 转成 Python 的 int,基本是零开销的。Python 内部用 30 位或 15 位 digit 存储小整数,超过这个范围会自动升级为任意精度。所以 Rust 的 i64 过去,Python 那边完全接得住。
| Rust 类型 | Python 类型 | 注意事项 |
|---|---|---|
| i8 / u8 | int | 范围小,一般够用 |
| i32 / u32 | int | 最常用,性能好 |
| i64 / u64 | int | 注意 Python 端无符号概念 |
| isize / usize | int | 平台相关,32位/64位不同 |
i64。它覆盖了绝大多数场景,而且 PyO3 对它的支持最成熟。
3. 浮点数类型:精度损失是隐形杀手
浮点数这块,Python 的 float 对应的是 C 的 double,也就是 Rust 的 f64。Python 没有 f32 的概念,所以 Rust 的 f32 传过去会被自动提升为 f64。
你想想看,这其实有个隐患:Rust 的 f32 精度只有 7 位有效数字,Python 的 float 有 15 位。从 Rust 往 Python 传 f32 时,那些「多出来」的精度位其实是随机填充的,容易让人误解。
3.1 双向转换示例
use pyo3::prelude::*;
#[pyfunction]
fn calculate_distance(x: f64, y: f64) -> f64 {
(x * x + y * y).sqrt()
}
#[pyfunction]
fn return_pi() -> f64 {
std::f64::consts::PI
}
Python 那边调用:
import my_module
result = my_module.calculate_distance(3.0, 4.0)
print(result) # 5.0
pi = my_module.return_pi()
print(pi) # 3.141592653589793
你看,浮点数的传递非常丝滑。但要注意:NaN 和 Infinity 也是可以传递的。Python 那边能识别 float('nan') 和 float('inf'),Rust 这边用 f64::NAN 和 f64::INFINITY 对应。
NaN,传回 Python 后,Python 端没做 math.isnan() 检查,直接拿去绘图,结果图上一片空白。排查了半天才发现是 NaN 在作怪。所以,跨语言传递浮点数时,建议两端都做 NaN 检查。
4. 布尔类型:简单但别大意
布尔类型是最简单的。Python 的 True/False 对应 Rust 的 true/false,一一对应,没有歧义。
use pyo3::prelude::*;
#[pyfunction]
fn is_even(x: i32) -> bool {
x % 2 == 0
}
#[pyfunction]
fn toggle(flag: bool) -> bool {
!flag
}
Python 调用:
print(my_module.is_even(4)) # True
print(my_module.is_even(5)) # False
print(my_module.toggle(True)) # False
不过有一点要注意:Python 里 bool 是 int 的子类。这意味着 True == 1 在 Python 里是成立的。但 Rust 这边,bool 和 i32 是彻底不同的类型,不能混用。如果你不小心把 1 传给了期望 bool 的 Rust 函数,PyO3 会报类型错误。
bool 而不是 i32 来表示布尔语义。这样代码自文档化,别人一看就知道这个参数是「是/否」的意思。
5. 字符串类型:所有权和编码是重头戏
字符串这块,水比较深。Python 的 str 是 Unicode 字符串,内部用 UTF-8 或 UTF-16 或 Latin-1 编码(取决于具体实现)。Rust 的 String 和 &str 则是严格的 UTF-8 编码。
说白了,两边都是 Unicode,但内部表示不同。PyO3 在转换时会做编码转换,这个过程有开销,但保证了正确性。
5.1 Python → Rust:字符串提取
use pyo3::prelude::*;
#[pyfunction]
fn greet(name: &str) -> String {
format!("你好,{}!", name)
}
#[pyfunction]
fn count_chars(text: String) -> usize {
text.chars().count() // 注意:不是 .len()
}
这里有个细节:&str 和 String 都可以作为参数类型。用 &str 表示只借用,不获取所有权;用 String 表示获取所有权。我个人习惯用 &str,除非我需要修改或存储这个字符串。
5.2 Rust → Python:字符串返回
#[pyfunction]
fn get_hello() -> String {
"Hello from Rust!".to_string()
}
#[pyfunction]
fn repeat_string(s: &str, n: usize) -> String {
s.repeat(n)
}
Python 调用:
print(my_module.get_hello()) # Hello from Rust!
print(my_module.repeat_string("Hi ", 3)) # Hi Hi Hi
字符串传递时,PyO3 会做一次内存拷贝。因为 Python 的字符串对象和 Rust 的字符串对象内存布局不同,必须复制。对于大字符串(比如几 MB 的文本),这个拷贝开销不可忽视。
PyBytes 传递原始字节,在 Rust 端自己解析,才把性能提上来。所以,超大字符串建议走字节流,别走 str 通道。
6. 综合对照表:一目了然
为了方便你查阅,我把四种基本类型的双向映射整理成了一张表。你写代码时可以随时回来翻看。
| Python 类型 | Rust 接收类型 | Rust 返回类型 | 注意事项 |
|---|---|---|---|
| int | i32, i64, u32, u64, isize, usize | i32, i64, u32, u64, isize, usize | 注意溢出,Python int 无上限 |
| float | f64(推荐), f32 | f64(推荐), f32 | f32 传回 Python 会升为 f64 |
| bool | bool | bool | Python 中 bool 是 int 子类,注意区分 |
| str | &str, String, Py<PyString> | String, &str, Py<PyString> | 大字符串注意拷贝开销 |
好了,这一章的内容就到这里。下一章我们会继续聊更复杂的数据类型——列表、字典、元组这些容器类型怎么跨语言传递。到时候你会发现,基本类型是基础,容器类型才是真正的挑战。
公众号:蓝海资料掘金营,微信deep3321