• 如何搭建一个结合Python和Rust代码的混合项目
  • 发布于 1个月前
  • 157 热度
    0 评论
本文将介绍如何搭建一个结合Python和Rust代码的混合项目。为何要结合?Python的解释型特性和动态类型能加快我们的开发流程。但面对更复杂的任务时,我们常常会遇到性能问题。

Rust是提升Python项目原生性能的理想选择。原生优化利用低级语言和编译器绕过Python解释器。通过结合Rust和Python,你能够兼取二者之长:既能利用Python获得快速且交互式的开发环境,又能借助Rust实现原生性能。

使用PyO3 + Maturin

将Rust与Python集成的主要方式是借助PyO3框架。利用PyO3,我们可以用Rust编写原生Python模块。当然,该框架也支持从Rust调用Python,但在这里我将只专注于用原生Rust模块扩展Python。PyO3会将你的Rust代码封装成一个原生Python模块。这样一来,绑定生成既简便又完全透明。

麻烦的部分在于构建Python与原生代码(Python + [Rust、C或C++])混合项目时的代码打包问题。那么,如何制作整合Python与原生代码的wheel包呢?
Python代码无需编译即可分发,并且与平台无关;安装wheel包时会即时生成.pyc文件(Python字节码)。但我们的Rust代码需要编译并作为共享库(二进制代码)进行分发。

打包是这个项目的难点:创建一种通用的、跨平台的方式来生成Python-Rust混合包。
能帮你实现这一目标的工具是Maturin。Maturin负责管理创建、构建、打包以及分发Python/Rust混合项目。

使用Maturin初始化混合项目

首先,使用pip安装Maturin Python包。
pip install maturin
它包含了maturin二进制文件,这是一个命令行界面。
# 堆代码 duidaima.com
$ maturin --help

maturin 0.12.9
Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as
python packages

USAGE:
    maturin <SUBCOMMAND>

OPTIONS:
-h,--help       Print help information
-V,--version    Print version information

SUBCOMMANDS:
    build          Build the crate into python packages
    develop        Installs the crateas module in the current virtualenv
    help           Print this message or the help of the given subcommand(s)
    init           Create a new cargo project in an existing directory
    list-python    Searches and lists the available python installations
    new            Create a new cargo project
    publish        Build and publish the crateas python packages to pypi
    sdist          Build only a source distribution(sdist) without compiling
    upload         Uploads python packages to pypi
maturin可执行文件提供了几个选项。让我们聚焦于build和develop这两个选项。
build:编译Rust代码并将其与Python包集成。它会生成一个wheel包,将Rust生成的二进制文件与最终的Python代码混合在一起。

develop:在开发和调试项目时非常有用。此命令构建新创建的共享库并直接将其安装到Python模块中。


为了测试Rust-Python混合项目,现在我们可以使用maturin new命令初始化我们的库。
$ maturin new --help
maturin-new
Create a new cargo project
USAGE:
    maturin new [OPTIONS]<PATH>
ARGS:
<PATH>Project path

OPTIONS:
-b,--bindings <BINDINGS>Which kind of bindings to use[possible values: pyo3, rust-cpython,
                                 cffi, bin]
-h,--help                   Print help information
--mixed                  Use mixed Rust/Python project layout
--name <NAME>Set the resulting package name, defaults to the directory name
对于我们的项目,通过使用选项 --bindings pyo3和 --mixed my_project来初始化一个带有PyO3绑定的Python/Rust混合项目。这里的my_project参数与本示例的目标项目目录相对应。
maturin new --bindings pyo3 --mixed my_project
生成的项目在my_project目录中集成了一个Python包、一个Rust项目定义文件Cargo.toml以及一个基于Rust的src目录。
$ tree my_project
  
my_project
├──Cargo.toml
├── my_project
│└── __init__.py
├── pyproject.toml
├── src
│└── lib.rs
└── test
└── test.py

3 directories,5 files
好了,我们已经有了基本的项目框架。现在可以添加一个简单的Rust函数来对外暴露了。

使用PyO3封装Python代码
我们需要在一个可被Python运行时调用的Python模块中声明并导出Rust函数。首先,使用#[pymodule] Rust宏创建一个模块。在我们的函数my_project中,将声明Rust与Python之间的绑定。
→ rust «my_project/src/lib.rs»=

use pyo3::prelude::*;(1)

(2)
<<functions>>

#[pymodule]
fnmy_project(_py:Python, m:&PyModule)->PyResult<()>{
(3)
<<function_declarations>>
Ok(())
}
(4)
<<tests>>
(1) 我们引入Py03的定义和宏。
(2) 在 <<函数>> 代码块中,声明我们的Rust函数。
(3) 在 <<函数声明>> 代码块中,在最终的Python模块中暴露我们的Rust函数。
(4) 在 <<测试>> 代码块中,添加Rust单元测试函数。

一个简单的函数
让我们从为 <<函数>> 代码块编写一个简单函数开始。我们要导出的函数is_prime通过用前面的数去除输入值来检查其是否为质数。对于一个数num,我们检查2到√num之间的数相除的余数。
→ rust «functions»=

#[pyfunction](1)
fnis_prime(num:u32)->bool{
match num {
0|1=>false,
        _ =>{
letlimit=(num asf32).sqrt()asu32;(2)

(2..=limit).any(|i| num % i ==0)==false(3)
}
}
}
(1) Rust宏#[pyfunction]会生成用于Python绑定的代码。
(2) 计算我们试除序列的上限。
(3) 进行试除并测试余数。2..=limit生成一个从2到limit(包含)的范围。any函数检查生成的元素中是否有一个满足条件。
在 <函数声明> 代码块中,将我们的函数添加到要导出的模块中。
→ rust «function_declarations» =
m.add_function(wrap_pyfunction!(is_prime, m)?)?;
在 <<测试>> 代码块中,添加一些简单的单元测试。
→ rust «tests»=

#[cfg(test)]
mod tests {
use super::*;

#[test]
fnsimple_test_false(){
assert_eq!(is_prime(0),false);
assert_eq!(is_prime(1),false);
assert_eq!(is_prime(12),false)
}

#[test]
fnsimple_test_true(){
assert_eq!(is_prime(2),true);
assert_eq!(is_prime(3),true);
assert_eq!(is_prime(41),true)
}
}
构建并运行你的Python模块
使用Maturin,构建原生Rust模块并在Python解释器中导出它只需一行命令。
$ cd my_project
$ maturin develop
这个命令构建原生Rust模块并将其部署到当前虚拟环境中。
import my_project
print(my_project.is_prime(12))
print(my_project.is_prime(11))
  
> False
> True
在幕后,maturin develop命令:
使用Cargo编译原生Rust模块:一个共享库被编译并复制到本地Python模块中。
安装Python模块:模块被安装到你的虚拟环境中。为了在每次Python代码更改时无需重新构建项目即可立即进行测试,你可以使用可编辑安装(即pip install -e),方法是在pyproject.yml(Maturin可编辑安装)中添加以下几行。
[build-system]
requires = ["maturin>=0.12"]
build-backend = "maturin"
测试你的模块
我们可以在Python中添加一个简单的基于属性的测试,以检查我们的is_prime Rust函数的行为。首先,为Hypothesis Python包添加一个依赖项。要向我们的项目添加依赖项,可以编辑pyproject.toml文件。
→ toml «my_project/pyproject.toml»=

[build-system]
requires =["maturin>=0.12"]
build-backend ="maturin"

[project.optional-dependencies]
test =[
"hypothesis",
"sympy"
]

[project]
name ="my_project"
requires-python =">=3.6"
classifiers =[
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
]
现在我们可以运行maturin develop命令。
$ cd my_project
$ maturin develop --extras test (1)
(1) 使用选项 --extras test,Maturin会安装Python测试依赖项。
我们添加一个简单的基于属性的测试。
→ Python«my_project/test/test.py»=

from hypothesis import settings,Verbosity, given
from hypothesis import strategies as st
from sympy.ntheory import isprime
import my_project

@given(s=st.integers(min_value=1, max_value=2**10))
@settings(verbosity=Verbosity.normal, max_examples=500)
def test_is_prime(s):
assertisprime(s)== my_project.is_prime(s)

if __name__ =="__main__":
test_is_prime()
最后,我们可以运行基于属性的Python测试。
$ cd my_project
$ python test/test.py
以及我们的Rust测试。
$ cd my_project
$ cargo test

总结
就是这样的!你的Python/Rust混合模块已经可以使用、部署和发布了。Maturin提供了几个命令来帮助编译和分发你的Rust/Python混合包。

有很多方法可以改进你的Python代码。可以直接在Python中使用Numba,或者在有类型的DSL中使用Cython。你可以使用绑定生成器暴露C/C++原生代码。但现在,借助PyO3,你可以在Python中受益于Rust语言的安全性以及原生性能。使用Maturin项目,创建Python + Rust混合项目变得很容易。最终项目被打包成一个包含Python代码和共享库的Python wheel包。
用户评论