Linux中國

結合使用 Python 和 Rust

RustPython 的優勢互補。可以使用 Python 進行原型設計,然後將性能瓶頸轉移到 Rust 上。

Python 和 Rust 是非常不同的語言,但它們實際上非常搭配。但在討論如何將 Python 與 Rust 結合之前,我想先介紹一下 Rust 本身。你可能已經聽說了這種語言,但可能還沒有了解過它的細節。

什麼是 Rust?

Rust 是一種低級語言,這意味著程序員所處理的東西接近於計算機的 「真實」 運行方式。

例如,整數類型由位元組大小定義,與 CPU 支持的類型相對應。雖然我們很想簡單地說 Rust 中的 a+b 對應於一條機器指令,但實際上並不完全是這樣!

Rust 編譯器鏈非常複雜。作為第一種近似的方法,將這樣的語句視為 「有點」 真實是有用的。

Rust 旨在實現零成本抽象,這意味著許多語言級別可用的抽象在運行時環境中會被編譯去掉。

例如,除非明確要求,對象會在堆棧上分配。結果是,在 Rust 中創建本地對象沒有運行時成本(儘管可能需要進行初始化)。

最後,Rust 是一種內存安全的語言。也有其他內存安全的語言和其他支持零成本抽象的語言。但通常這些是兩類不同的語言。

內存安全並不意味著不可能在 Rust 中出現內存違規。它確實意味著只有兩種方式可能導致內存違規:

  • 編譯器的錯誤。
  • 顯式聲明為不安全(unsafe)的代碼。

Rust 標準庫代碼有很多被標記為不安全的代碼,雖然比許多人預期的少。這並不意味著該語句無意義。除了需要自己編寫不安全代碼的(罕見的)情況外,內存違規通常是由基礎設施造成的。

為什麼會有 Rust 出現?

為什麼人們要創建 Rust?是哪些問題沒有被現有編程語言解決嗎?

Rust 被設計成既能高效運行,又保證內存安全。在現代的聯網世界中,這是一個越來越重要的問題。

Rust 的典型應用場景是協議的低級解析。待解析的數據通常來自不受信任的來源,並且需要通過高效的方式進行解析。

如果你認為這聽起來像 Web 瀏覽器所做的事情,那不是巧合。Rust 最初起源於 Mozilla 基金會,它是為了改進 Firefox 瀏覽器而設計的。

如今,需要保證安全和速度的不僅僅是瀏覽器。即使是常見的微服務架構也必須能夠快速解析不受信任的數據,同時保證安全。

現實示例:統計字元

為了理解 「封裝 Rust」 的例子,需要解決一個問題。這個問題需要滿足以下要求:

  • 足夠容易解決。
  • 能夠寫高性能循環來優化。
  • 有一定的現實意義。

這個玩具問題的例子是判斷一個字元在一個字元串中是否出現超過了 X 次。這個問題不容易通過高效的正則表達式解決。即使是專門的 Numpy 代碼也可能不夠快,因為通常沒有必要掃描整個字元串。

你可以想像一些 Python 庫和技巧的組合來解決這個問題。然而,如果在低級別的語言中實現直接的演算法,它會非常快,並且更易於閱讀。

為了使問題稍微有趣一些,以演示 Rust 的一些有趣部分,這個問題增加了一些變化。該演算法支持在換行符處重置計數(意即:字元是否在一行中出現了超過 X 次?)或在空格處重置計數(意即:字元是否在單詞中出現了超過 X 次?)。

這是唯一與 「現實性」 相關的部分。過多的現實性將使這個示例在教育上不再有用。

支持枚舉

Rust 支持使用枚舉(enum)。你可以使用枚舉做很多有趣的事情。

目前,只使用了一個簡單的三選一的枚舉,並沒有其他的變形。這個枚舉編碼了哪種字元重置計數。

#[derive(Copy)]
enum Reset {
    NewlinesReset,
    SpacesReset,
    NoReset,
}

支持結構

接下來的 Rust 組件更大一些:這是一個結構(struct)。Rust 的結構與 Python 的 dataclass 有些相似。同樣,你可以用結構做更複雜的事情。

#[pyclass]
struct Counter {
    what: char,
    min_number: u64,
    reset: Reset, 
}

實現塊

你可以在 Rust 中使用一個單獨的塊,稱為實現(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 變數是常量。由於當前的計數(current_count)必須更改,因此它被聲明為可變變數。

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 代碼

為了將 Rust 代碼封裝到 Python 中,你可以使用 PyO3。PyO3 Rust 「crate」(即庫)允許內聯提示將 Rust 代碼包裝為 Python,使得修改兩者更容易。

包含 PyO3 crate 原語

首先,你必須包含 PyO3 crate 原語。

use pyo3::prelude::*;

封裝枚舉

枚舉需要被封裝。derive 從句對於將枚舉封裝為 PyO3 是必需的,因為它們允許類被複制和克隆,使它們更容易在 Python 中使用。

#[pyclass]
#[derive(Clone)]
#[derive(Copy)]
enum Reset {
    /* ... */
}

封裝結構

結構同樣需要被封裝。在 Rust 中,這些被稱為 「宏」,它們會生成所需的介面位。

#[pyclass]
struct Counter {
    /* ... */
}

封裝實現

封裝實現(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 輪子,它可以上傳到 PyPI。輪子是特定於 CPU 架構的。

Python 庫

從 Python 中使用庫是最簡單的部分。沒有任何東西表明這與在 Python 中編寫代碼有什麼區別。這其中的一個有用方面是,如果你優化了已經有單元測試的 Python 中的現有庫,你可以使用 Python 單元測試來測試 Rust 庫。

導入

無論你是使用 maturin develop 還是 pip install 來安裝它,導入庫都是使用 import 完成的。

import counter

構造函數

構造函數的定義正好使對象可以從 Python 構建。這並不總是如此。有時僅從更複雜的函數返回對象。

cntr = counter.Counter(
    &apos;c&apos;,
    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,你的開發和部署流程更容易進行。開發、構建並享受這一組合吧!

via: https://opensource.com/article/23/3/python-loves-rust

作者:Moshe Zadka 選題:lkxed 譯者:ChatGPT 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國