Python 和 Rust 是非常不同的语言,但它们实际上非常互补。但在讨论如何将 Python 与 Rust 结合使用之前,我想先介绍一下 Rust 本身。您可能听说过这门语言,但可能没有听说过关于其工作原理的详细信息。
什么是 Rust?
Rust 是一种底层语言。这意味着程序员处理的事情与计算机“真正”的工作方式非常接近。
例如,整数类型由位大小定义,并对应于 CPU 支持的类型。虽然很容易说这意味着 Rust 中的 a+b
对应于一条机器指令,但实际上并非完全如此!
Rust 的编译器链非常复杂。将其作为第一个近似值来对待,将此类语句视为“某种程度上”为真是有用的。
Rust 被设计为零成本抽象,这意味着语言级别提供的许多抽象在运行时会被编译掉。
例如,对象在堆栈上分配,除非明确要求。结果是,在 Rust 中创建本地对象没有运行时成本(尽管初始化可能有)。
最后,Rust 是一种内存安全语言。还有其他内存安全语言和其他零成本抽象语言。通常,这些是不同的语言。
内存安全并不意味着在 Rust 中不可能发生内存违规。它确实意味着只有两种方式可能发生内存违规:
- 编译器的错误。
- 显式声明为不安全的代码。
Rust 标准库代码中有相当多的代码被标记为不安全,尽管比许多人假设的要少。但这并没有使该声明变得空洞。除了(极少数)需要自己编写不安全代码的情况外,内存违规是由底层基础设施引起的。
Rust 为何存在?
人们为什么要创建 Rust?现有语言未能解决什么问题?
Rust 被设计为一种实现高性能代码和内存安全的语言的组合。在网络世界中,这种关注变得越来越重要。
Rust 的典型用例是协议的底层解析。要解析的数据通常来自不受信任的来源,可能需要以高性能的方式进行解析。
如果这听起来像 Web 浏览器所做的事情,那并非巧合。Rust 起源于 Mozilla 基金会,旨在改进 Firefox 浏览器。
在现代世界中,浏览器不再是唯一需要安全和快速的东西。即使是常见的微服务架构,加上纵深防御原则,也必须能够快速解包不受信任的数据。
字符计数
要理解“Rust 封装”示例,需要解决一个问题。不仅仅是任何问题都可以。问题需要是:
- 足够容易解决
- 能够通过编写高性能循环来帮助解决
- 在某种程度上是真实的
本例中的玩具问题是某个字符在字符串中出现的次数是否超过 X 次。这对于高性能正则表达式来说并不容易。即使是专用的 Numpy 代码也可能比必要的慢,因为通常不需要扫描整个字符串。
您可以想象 Python 库和技巧的某种组合可以实现这一点。但是,如果用底层语言实现,显而易见的算法非常快,并且使代码更具可读性。
添加了一个小小的变化,使问题稍微有趣一些,并演示 Rust 的一些有趣部分。该算法支持在新行(字符在一行中出现的次数是否超过 X 次?)或空格(字符在一个单词中出现的次数是否超过 X 次?)上重置计数。
这是对“真实性”的唯一致敬。任何更多的真实性都会使该示例在教学上无用。
枚举支持
Rust 支持枚举(enums
)。您可以使用 enums
做很多有趣的事情。
现在,使用一个三向 enum
,没有任何其他花哨的东西。enum
编码了哪个字符重置计数。
#[derive(Copy)]
enum Reset {
NewlinesReset,
SpacesReset,
NoReset,
}
结构体支持
下一个 Rust 组件更重要一些:一个 struct
。Rust struct
有点类似于 Python dataclass
。同样,您可以使用 struct
做更复杂的事情。
#[pyclass]
struct Counter {
what: char,
min_number: u64,
reset: Reset,
}
实现块
您可以在 Rust 中的单独块中向 struct
添加方法:impl
块。详细信息超出了本文的范围。
在本例中,该方法调用了一个外部函数。这主要是为了分解代码。更复杂的用法是指示 Rust 编译器内联该函数,以允许在不产生任何运行时成本的情况下提高可读性。
#[pymethods]
impl Counter {
#[new]
fn new(what: char, min_number: u64, reset: Reset) -> Self {
Counter{what: what, min_number: min_number, reset: reset}
}
fn has_count(
&self,
data: &str,
) -> bool {
has_count(self, data.chars())
}
}
函数
默认情况下,Rust 变量是常量。由于当前计数必须更改,因此将其声明为可变变量。
fn has_count(cntr: &Counter, chars: std::str::Chars) -> bool {
let mut current_count : u64 = 0;
for c in chars {
if got_count(cntr, c, &mut current_count) {
return true;
}
}
false
}
循环遍历字符并调用函数 got_count
。同样,这样做是为了将代码分解为幻灯片。它确实展示了如何将可变借用引用发送到函数。
即使 current_count
是可变的,发送和接收站点都显式地将引用标记为可变的。这清楚地表明了哪些函数可能会修改值。
计数
got_count
重置计数器,递增计数器,然后检查它。Rust 的冒号分隔的表达式序列会评估为最后一个表达式的结果,在本例中,即是否达到阈值。
fn got_count(cntr: &Counter, c: char, current_count: &mut u64) -> bool {
maybe_reset(cntr, c, current_count);
maybe_incr(cntr, c, current_count);
*current_count >= cntr.min_number
}
重置代码
reset
代码显示了 Rust 中另一个有用的东西:匹配。对 Rust 中匹配能力的完整描述将是一个学期的课程,而不是在不相关的演讲中花两分钟,但此示例在元组上匹配,匹配两个选项之一。
fn maybe_reset(cntr: &Counter, c: char, current_count: &mut u64) -> () {
match (c, cntr.reset) {
('\n', Reset::NewlinesReset) | (' ', Reset::SpacesReset)=> {
*current_count = 0;
}
_ => {}
};
}
递增支持
递增将字符与所需的字符进行比较,如果匹配,则递增计数。
fn maybe_incr(cntr: &Counter, c: char, current_count: &mut u64) -> (){
if c == cntr.what {
*current_count += 1;
};
}
请注意,我优化了本文中的代码以用于幻灯片。它不一定是 Rust 代码的最佳实践示例,也不是如何设计良好 API 的示例。
为 Python 封装 Rust 代码
要为 Python 封装 Rust 代码,您可以使用 PyO3。PyO3 Rust “crate”(或库)允许内联提示将 Rust 代码封装到 Python 中,从而更容易一起修改两者。
包含 PyO3 crate 原语
首先,您必须包含 PyO3 crate 原语。
use pyo3::prelude::*;
封装枚举
enum
需要被封装。derive
子句对于为 PyO3 封装 enum
是必要的,因为它们允许复制和克隆类,使其更容易从 Python 中使用。
#[pyclass]
#[derive(Clone)]
#[derive(Copy)]
enum Reset {
/* ... */
}
封装结构体
struct
也以类似的方式封装。这些调用 Rust 中的“宏”,它们生成所需的接口位。
#[pyclass]
struct Counter {
/* ... */
}
封装 impl
封装 impl
更有趣。添加了另一个名为 new
的宏。此方法标记为 #[new]
,让 PyO3 知道如何为内置对象公开构造函数。
#[pymethods]
impl Counter {
#[new]
fn new(what: char, min_number: u64,
reset: Reset) -> Self {
Counter{what: what,
min_number: min_number, reset: reset}
}
/* ... */
}
定义模块
最后,定义一个初始化模块的函数。此函数具有特定的签名,必须与模块同名,并使用 #[pymodule]
修饰。
#[pymodule]
fn counter(_py: Python, m: &PyModule
) -> PyResult<()> {
m.add_class::<Counter>()?;
m.add_class::<Reset>()?;
Ok(())
}
?
表示此函数可能会失败(例如,如果类配置不当)。PyResult
在导入时被转换为 Python 异常。
Maturin 开发
为了快速检查,maturin develop
构建库并将其安装到当前虚拟环境中。这有助于快速迭代。
$ maturin develop
Maturin 构建
maturin build
命令构建一个 manylinux
wheel,可以上传到 PyPI。该 wheel 针对特定的 CPU 架构。
Python 库
从 Python 中使用该库是很棒的部分。没有任何迹象表明这与用 Python 编写代码有任何不同。这方面的一个有用之处是,如果您优化了 Python 中已经具有单元测试的现有库,则可以将 Python 单元测试用于 Rust 库。
导入
无论您使用 maturin develop
还是 pip install
安装它,导入库都使用 import
完成。
import counter
构造
构造函数被精确地定义,以便可以从 Python 构建对象。情况并非总是如此。有时对象仅从更复杂的函数返回。
cntr = counter.Counter(
'c',
3,
counter.Reset.NewlinesReset,
)
调用
最终的回报终于来了。检查此字符串是否至少有三个“c”字符
>>> cntr.has_count("hello-c-c-c-goodbye")
True
添加换行符会导致其余部分发生,并且在没有插入换行符的情况下没有三个“c”字符
>>> cntr.has_count("hello-c-c-\nc-goodbye")
False
使用 Rust 和 Python 很简单
我的目标是说服您,将 Rust 和 Python 结合使用很容易。我编写了少量代码来“粘合”它们。Rust 和 Python 具有互补的优势和劣势。
Rust 非常适合高性能、安全的代码。Rust 具有陡峭的学习曲线,并且对于快速原型化解决方案来说可能很笨拙。
Python 很容易上手,并且支持非常紧密的迭代循环。Python 确实有一个“速度上限”。超过一定水平,从 Python 获得更好的性能就更难了。
将它们结合起来是完美的。在 Python 中进行原型设计,并将性能瓶颈转移到 Rust。
使用 maturin
,您的开发和部署管道更容易构建。开发、构建,并享受这种组合!
评论已关闭。