为什么你应该一起使用 Python 和 Rust

Rust 和 Python 具有互补的优势和劣势。在 Python 中进行原型设计,并将性能瓶颈转移到 Rust。
3 位读者喜欢这篇文章。
Ferris the crab under the sea, unofficial logo for Rust programming language

Opensource.com

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,您的开发和部署管道更容易构建。开发、构建,并享受这种组合!

标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe 自 1998 年以来一直参与 Linux 社区,帮助举办 Linux “安装聚会”。他自 1999 年以来一直编写 Python 程序,并为核心 Python 解释器做出了贡献。Moshe 在这些术语存在之前就一直是 DevOps/SRE,他非常关心软件可靠性、构建可重现性以及其他此类事情。

评论已关闭。

Creative Commons License本作品根据 Creative Commons 许可协议 授权。 署名-相同方式共享 4.0 国际许可协议。
© 2025 open-source.net.cn. All rights reserved.