Python调用Rust研究

方法

有使用maturin

将rust编译成so/dll文件,并在python中调用so/dll文件

使用maturin

先创建python项目的虚拟环境

激活项目虚拟环境

在虚拟环境里装maturin

1
pip install maturin

使用它初始化python项目

1
maturin init

然后开始编写rust,可以在它的基础上进行更改

然后运行

1
maturin develop

然后python这里就能调用了

直接让rust编译成二进制文件

PyO3: Python 调用 Rust 代码 - 编程宝库 (codebaoku.com)

这个经过测试可以实现windows上,python调用rust编译成的dll,将dll改名成pyd。

但一定要保证最终 #[pymodule] 修饰的那个函数的名字,和最终python里import的名字保持一样

还要用windows11本身的cmd黑色窗口来运行,用rustRover里面的编译指令可能会出问题。

注意:打包成so文件的,必须要用linux环境运行,并且打包出来的so文件前面会多一个lib三个字符,需要去掉。

新问题:

rust这里代码量多了如何组织?分别打包成多个不同的模块,还是集成在一起?

rust这里写的类型在python那里用的时候有什么局限性?(int变成i32会有大小限制)

这种变成二进制文件的引用调用中间会不会有速度损耗?相比纯python编写的逻辑,性能提升了多少?

除了调用函数以外,还有没有其他的东西?比如从rust这里拿到一个类,提供一个类让python这里去用(很麻烦)

类型局限性

1
2
3
4
5
6
7
Traceback (most recent call last):
File "D:\Projects\Project-Test\python-rust-so\main.py", line 12, in <module>
main()
File "D:\Projects\Project-Test\python-rust-so\main.py", line 7, in main
print(rust_rover_test.sum_as_string(100000000000000000000000000000000000000000, 1000_0000_0000_0000))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OverflowError: int too big to convert

在python端传入太大的数字会崩掉。

pyo3的官方文档

Introduction - PyO3 user guide

发现一个警告:最开始的模块绑定所有函数的代码是这样的:

1
2
3
4
5
6
#[pymodule]
fn rust_rover_test(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(sum_as_string))?;
m.add_wrapped(wrap_pyfunction!(sum_i32))?;
Ok(())
}

后来发现原来是API过时了,变了,参数应该改成

1
2
3
4
5
6
7
8
#[pymodule]
fn rust_rover_test(m: &Bound<'_, PyModule>) -> PyResult<()> {
// m.add_wrapped(wrap_pyfunction!(sum_as_string))?;
// m.add_wrapped(wrap_pyfunction!(sum_i32))?;
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
m.add_function(wrap_pyfunction!(sum_i32, m)?)?;
Ok(())
}

这个是在pyo3官网上看到的

对数据类型的对接探索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
use std::collections::{HashMap, HashSet};
// lib.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}

#[pyfunction]
fn sum_i32(a: i32, b: i32) -> PyResult<i32> {
Ok(a + b)
}

#[pyfunction]
fn sum_f32(a: f32, b: f32) -> PyResult<f32> {
Ok(a + b)
}

/// 将两个列表合并成一个列表的函数
/// 这说明 Vec<i32> 类型可以被转换为 Python 列表 List[int]
#[pyfunction]
fn merge_lists(a: Vec<i32>, b: Vec<i32>) -> PyResult<Vec<i32>> {
Ok(a.iter().chain(b.iter()).cloned().collect())
}

/// 将两个元组合并成一个元组的函数
/// 这说明 (i32, i32) 类型可以被转换为 Python 元组 Tuple[int, int]
#[pyfunction]
fn merge_tuples(a: (i32, i32), b: (i32, i32)) -> PyResult<(i32, i32)> {
Ok((a.0 + b.0, a.1 + b.1))
}

/// 将两个字典合并成一个字典的函数
/// 这说明 HashMap<String, i32> 类型可以被转换为 Python 字典 Dict[str, int]
#[pyfunction]
fn merge_dicts(a: HashMap<String, i32>, b: HashMap<String, i32>) -> PyResult<HashMap<String, i32>> {
let mut result = a;
for (k, v) in b {
result.insert(k, v);
}
Ok(result)
}

/// 将两个集合合并成一个集合的函数
/// 这说明 HashSet<i32> 类型可以被转换为 Python 集合 Set[int]
#[pyfunction]
fn merge_sets(a: HashSet<i32>, b: HashSet<i32>) -> PyResult<HashSet<i32>> {
Ok(a.union(&b).cloned().collect())
}

#[pymodule]
fn rust_rover_test(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
m.add_function(wrap_pyfunction!(sum_i32, m)?)?;
m.add_function(wrap_pyfunction!(sum_f32, m)?)?;
m.add_function(wrap_pyfunction!(merge_lists, m)?)?;
m.add_function(wrap_pyfunction!(merge_tuples, m)?)?;
m.add_function(wrap_pyfunction!(merge_dicts, m)?)?;
m.add_function(wrap_pyfunction!(merge_sets, m)?)?;
Ok(())
}

对组合数据类型的摸索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
use std::collections::{HashMap, HashSet};
// lib.rs
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}

#[pyfunction]
fn sum_i32(a: i32, b: i32) -> PyResult<i32> {
Ok(a + b)
}

#[pyfunction]
fn sum_f32(a: f32, b: f32) -> PyResult<f32> {
Ok(a + b)
}

/// 将二维数组(矩阵)所有的值相加并返回
/// 输入:二维数组(矩阵)
/// 输出:所有元素之和
#[pyfunction]
fn matrix_sum(matrix: Vec<Vec<i32>>) -> PyResult<i32> {
let mut sum = 0;
for row in matrix {
for num in row {
sum += num;
}
}
Ok(sum)
}

/// 将字典列表 Dict[int, List[int]] 转换为 HashMap<int, HashSet<int>>
/// 输入:字典列表
/// 输出:HashMap<int, HashSet<int>>
#[pyfunction]
fn dict_to_hashmap(dict_list: Vec<HashMap<i32, Vec<i32>>>) -> PyResult<HashMap<i32, HashSet<i32>>> {
let mut hashmap = HashMap::new();
for mut dict in dict_list {
for (key, value) in dict.drain() {
hashmap.entry(key).or_insert_with(HashSet::new).extend(value);
}
}
Ok(hashmap)
}


#[pymodule]
fn rust_rover_test(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
m.add_function(wrap_pyfunction!(sum_i32, m)?)?;
m.add_function(wrap_pyfunction!(sum_f32, m)?)?;
m.add_function(wrap_pyfunction!(matrix_sum, m)?)?;
m.add_function(wrap_pyfunction!(dict_to_hashmap, m)?)?;
Ok(())
}

实际上,类型转换官网上有一个表格:

Rust 类型到 Python 类型的映射 - PyO3 用户指南 — Mapping of Rust types to Python types - PyO3 user guide

实际上运行的时候,python参数会转换成rust参数,然后在rust代码中运行。这个是有一点点转换成本的。但是rust的高性能会极大的弥补这成本。

代码组织问题

直接把src/lib.rs 当成main文件,只写一个注册函数的总函数。其他的引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::collections::{HashMap, HashSet};
use pyo3::{pyfunction, PyResult};

/// 将字典列表 Dict[int, List[int]] 转换为 HashMap<int, HashSet<int>>
/// 输入:字典列表
/// 输出:HashMap<int, HashSet<int>>
#[pyfunction]
pub fn dict_to_hashmap(dict_list: Vec<HashMap<i32, Vec<i32>>>) -> PyResult<HashMap<i32, HashSet<i32>>> {
let mut hashmap = HashMap::new();
for mut dict in dict_list {
for (key, value) in dict.drain() {
hashmap.entry(key).or_insert_with(HashSet::new).extend(value);
}
}
Ok(hashmap)
}

比如这个函数,前面就要注意加上pub关键字。

然后lib.rs里面,直接引入

1
2
3
// 引入的内容
mod dict_functions;
use dict_functions::dict_to_hashmap;

直接调用就可以了。比较好的是,这个函数的rust里的三斜杠注释文档会能被python这里提取识别到

1
print(rust_rover_test.dict_to_hashmap.__doc__)

即使有了pyi文件,pyi文件里写的python文档注释,是给pycharm看的,鼠标悬浮在函数上显示用的,但实际上 __doc__ 和help函数,调用的是rust里面的文档注释和函数参数名。

但是在python调用端,如何像这样含有很多点调用,看起来有组织结构?

1
2
res = DictUtils.dict_to_hashmap({...})
res = GameBoardUtils.transform_board([...])

性能测试

写了一个五子棋判断输赢的代码,看rust比python快多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from random import randint
from time import perf_counter
import rust_rover_test


def generate_random_board():
"""
生成随机落子的15*15棋盘
0表示空格,1表示黑子,2表示白子
:return:
"""
return [[randint(0, 2) for _ in range(15)] for _ in range(15)]


def main():
boards = [generate_random_board() for _ in range(10_0000)]
print("Start testing...")
t1 = perf_counter()
res = [0, 0, 0]
for board in boards:
# winner = rust_rover_test.get_score(board)
winner = check_winner(board)
res[winner] += 1
t2 = perf_counter()
print(res)
print(f"Time used: {t2 - t1:.6f}s")

res = [0, 0, 0]

# 对比两个函数的运行时间
t1 = perf_counter()
for board in boards:
winner = rust_rover_test.get_score(board)
# winner = check_winner(board)
res[winner] += 1
t2 = perf_counter()
print(res)
print(f"Time used: {t2 - t1:.6f}s")
pass


测试代码是这样的。

具体检测原理的代码就不写了,因为是直接拿AI写的,懒得自己写了。但AI写的两个代码跑的结果居然不一样。不去纠结这个细节了。

测试结果显示这样的代码,rust比python快四倍。python用0.166秒,rust用0.4817秒