結(jié)合使用 Python 和 Rust
Python 和 Rust 是非常不同的語(yǔ)言,但它們實(shí)際上非常搭配。但在討論如何將 Python 與 Rust 結(jié)合之前,我想先介紹一下 Rust 本身。你可能已經(jīng)聽(tīng)說(shuō)了這種語(yǔ)言,但可能還沒(méi)有了解過(guò)它的細(xì)節(jié)。
什么是 Rust?
Rust 是一種低級(jí)語(yǔ)言,這意味著程序員所處理的東西接近于計(jì)算機(jī)的 “真實(shí)” 運(yùn)行方式。
例如,整數(shù)類型由字節(jié)大小定義,與 CPU 支持的類型相對(duì)應(yīng)。雖然我們很想簡(jiǎn)單地說(shuō) Rust 中的 a+b
對(duì)應(yīng)于一條機(jī)器指令,但實(shí)際上并不完全是這樣!
Rust 編譯器鏈非常復(fù)雜。作為第一種近似的方法,將這樣的語(yǔ)句視為 “有點(diǎn)” 真實(shí)是有用的。
Rust 旨在實(shí)現(xiàn)零成本抽象,這意味著許多語(yǔ)言級(jí)別可用的抽象在運(yùn)行時(shí)環(huán)境中會(huì)被編譯去掉。
例如,除非明確要求,對(duì)象會(huì)在堆棧上分配。結(jié)果是,在 Rust 中創(chuàng)建本地對(duì)象沒(méi)有運(yùn)行時(shí)成本(盡管可能需要進(jìn)行初始化)。
最后,Rust 是一種內(nèi)存安全的語(yǔ)言。也有其他內(nèi)存安全的語(yǔ)言和其他支持零成本抽象的語(yǔ)言。但通常這些是兩類不同的語(yǔ)言。
內(nèi)存安全并不意味著不可能在 Rust 中出現(xiàn)內(nèi)存違規(guī)。它確實(shí)意味著只有兩種方式可能導(dǎo)致內(nèi)存違規(guī):
- 編譯器的錯(cuò)誤。
- 顯式聲明為不安全(
unsafe
)的代碼。
Rust 標(biāo)準(zhǔn)庫(kù)代碼有很多被標(biāo)記為不安全的代碼,雖然比許多人預(yù)期的少。這并不意味著該語(yǔ)句無(wú)意義。除了需要自己編寫不安全代碼的(罕見(jiàn)的)情況外,內(nèi)存違規(guī)通常是由基礎(chǔ)設(shè)施造成的。
為什么會(huì)有 Rust 出現(xiàn)?
為什么人們要?jiǎng)?chuàng)建 Rust?是哪些問(wèn)題沒(méi)有被現(xiàn)有編程語(yǔ)言解決嗎?
Rust 被設(shè)計(jì)成既能高效運(yùn)行,又保證內(nèi)存安全。在現(xiàn)代的聯(lián)網(wǎng)世界中,這是一個(gè)越來(lái)越重要的問(wèn)題。
Rust 的典型應(yīng)用場(chǎng)景是協(xié)議的低級(jí)解析。待解析的數(shù)據(jù)通常來(lái)自不受信任的來(lái)源,并且需要通過(guò)高效的方式進(jìn)行解析。
如果你認(rèn)為這聽(tīng)起來(lái)像 Web 瀏覽器所做的事情,那不是巧合。Rust 最初起源于 Mozilla 基金會(huì),它是為了改進(jìn) Firefox 瀏覽器而設(shè)計(jì)的。
如今,需要保證安全和速度的不僅僅是瀏覽器。即使是常見(jiàn)的微服務(wù)架構(gòu)也必須能夠快速解析不受信任的數(shù)據(jù),同時(shí)保證安全。
現(xiàn)實(shí)示例:統(tǒng)計(jì)字符
為了理解 “封裝 Rust” 的例子,需要解決一個(gè)問(wèn)題。這個(gè)問(wèn)題需要滿足以下要求:
- 足夠容易解決。
- 能夠?qū)懜咝阅苎h(huán)來(lái)優(yōu)化。
- 有一定的現(xiàn)實(shí)意義。
這個(gè)玩具問(wèn)題的例子是判斷一個(gè)字符在一個(gè)字符串中是否出現(xiàn)超過(guò)了 X 次。這個(gè)問(wèn)題不容易通過(guò)高效的正則表達(dá)式解決。即使是專門的 Numpy 代碼也可能不夠快,因?yàn)橥ǔ](méi)有必要掃描整個(gè)字符串。
你可以想象一些 Python 庫(kù)和技巧的組合來(lái)解決這個(gè)問(wèn)題。然而,如果在低級(jí)別的語(yǔ)言中實(shí)現(xiàn)直接的算法,它會(huì)非???,并且更易于閱讀。
為了使問(wèn)題稍微有趣一些,以演示 Rust 的一些有趣部分,這個(gè)問(wèn)題增加了一些變化。該算法支持在換行符處重置計(jì)數(shù)(意即:字符是否在一行中出現(xiàn)了超過(guò) X 次?)或在空格處重置計(jì)數(shù)(意即:字符是否在單詞中出現(xiàn)了超過(guò) X 次?)。
這是唯一與 “現(xiàn)實(shí)性” 相關(guān)的部分。過(guò)多的現(xiàn)實(shí)性將使這個(gè)示例在教育上不再有用。
支持枚舉
Rust 支持使用枚舉(enum
)。你可以使用枚舉做很多有趣的事情。
目前,只使用了一個(gè)簡(jiǎn)單的三選一的枚舉,并沒(méi)有其他的變形。這個(gè)枚舉編碼了哪種字符重置計(jì)數(shù)。
#[derive(Copy)]
enum Reset {
NewlinesReset,
SpacesReset,
NoReset,
}
支持結(jié)構(gòu)
接下來(lái)的 Rust 組件更大一些:這是一個(gè)結(jié)構(gòu)(struct
)。Rust 的結(jié)構(gòu)與 Python 的 dataclass
有些相似。同樣,你可以用結(jié)構(gòu)做更復(fù)雜的事情。
#[pyclass]
struct Counter {
what: char,
min_number: u64,
reset: Reset,
}
實(shí)現(xiàn)塊
你可以在 Rust 中使用一個(gè)單獨(dú)的塊,稱為實(shí)現(xiàn)(impl
)塊,為結(jié)構(gòu)添加一個(gè)方法。但具體細(xì)節(jié)超出了本文的范圍。
在這個(gè)示例中,該方法調(diào)用了一個(gè)外部函數(shù)。這主要是為了分解代碼。更復(fù)雜的用例將指示 Rust 編譯器內(nèi)聯(lián)該函數(shù),以便在不產(chǎn)生任何運(yùn)行時(shí)成本的情況下提高可讀性。
#[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())
}
}
函數(shù)
默認(rèn)情況下,Rust 變量是常量。由于當(dāng)前的計(jì)數(shù)(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
}
該循環(huán)遍歷字符并調(diào)用 got_count
函數(shù)。再次強(qiáng)調(diào),這是為了將代碼分解成幻燈片展示。它展示了如何向函數(shù)發(fā)送可變引用。
盡管 current_count
是可變的,但發(fā)送和接收站點(diǎn)都顯式標(biāo)記該引用為可變。這可以清楚地表明哪些函數(shù)可能修改一個(gè)值。
計(jì)數(shù)
got_count
函數(shù)重置計(jì)數(shù)器,將其遞增,然后檢查它。Rust 的冒號(hào)分隔的表達(dá)式序列評(píng)估最后一個(gè)表達(dá)式的結(jié)果,即是否達(dá)到了指定的閾值。
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 中另一個(gè)有用的功能:模式匹配。對(duì) Rust 中匹配的完整描述需要一個(gè)學(xué)期級(jí)別的課程,不適合在一個(gè)無(wú)關(guān)的演講中講解。這個(gè)示例匹配了該元組的兩個(gè)選項(xiàng)之一。
fn maybe_reset(cntr: &Counter, c: char, current_count: &mut u64) -> () {
match (c, cntr.reset) {
('\n', Reset::NewlinesReset) | (' ', Reset::SpacesReset)=> {
*current_count = 0;
}
_ => {}
};
}
增量支持
增量將字符與所需字符進(jìn)行比較,并在匹配時(shí)增加計(jì)數(shù)。
fn maybe_incr(cntr: &Counter, c: char, current_count: &mut u64) -> (){
if c == cntr.what {
*current_count += 1;
};
}
請(qǐng)注意,我在本文中優(yōu)化了代碼以適合幻燈片。這不一定是 Rust 代碼的最佳實(shí)踐示例,也不是如何設(shè)計(jì)良好的 API 的示例。
為 Python 封裝 Rust 代碼
為了將 Rust 代碼封裝到 Python 中,你可以使用 PyO3。PyO3 Rust “crate”(即庫(kù))允許內(nèi)聯(lián)提示將 Rust 代碼包裝為 Python,使得修改兩者更容易。
包含 PyO3 crate 原語(yǔ)
首先,你必須包含 PyO3 crate 原語(yǔ)。
use pyo3::prelude::*;
封裝枚舉
枚舉需要被封裝。derive
從句對(duì)于將枚舉封裝為 PyO3 是必需的,因?yàn)樗鼈冊(cè)试S類被復(fù)制和克隆,使它們更容易在 Python 中使用。
#[pyclass]
#[derive(Clone)]
#[derive(Copy)]
enum Reset {
/* ... */
}
封裝結(jié)構(gòu)
結(jié)構(gòu)同樣需要被封裝。在 Rust 中,這些被稱為 “宏”,它們會(huì)生成所需的接口位。
#[pyclass]
struct Counter {
/* ... */
}
封裝實(shí)現(xiàn)
封裝實(shí)現(xiàn)(impl
)更有趣。增加了另一個(gè)名為 new
的宏。此方法被標(biāo)記為 #[new]
,讓 PyO3 知道如何為內(nèi)置對(duì)象公開(kāi)構(gòu)造函數(shù)。
#[pymethods]
impl Counter {
#[new]
fn new(what: char, min_number: u64,
reset: Reset) -> Self {
Counter{what: what,
min_number: min_number, reset: reset}
}
/* ... */
}
定義模塊
最后,定義一個(gè)初始化模塊的函數(shù)。此函數(shù)具有特定的簽名,必須與模塊同名,并用 #[pymodule]
修飾。
#[pymodule]
fn counter(_py: Python, m: &PyModule
) -> PyResult<()> {
m.add_class::<Counter>()?;
m.add_class::<Reset>()?;
Ok(())
}
?
顯示此函數(shù)可能失敗(例如,如果類沒(méi)有正確配置)。 PyResult
在導(dǎo)入時(shí)轉(zhuǎn)換為 Python 異常。
Maturin 開(kāi)發(fā)
為了快速檢查,用 maturin develop
構(gòu)建并將庫(kù)安裝到當(dāng)前虛擬環(huán)境中。這有助于快速迭代。
$ maturin develop
Maturin 構(gòu)建
maturin build
命令構(gòu)建一個(gè) manylinux
輪子,它可以上傳到 PyPI。輪子是特定于 CPU 架構(gòu)的。
Python 庫(kù)
從 Python 中使用庫(kù)是最簡(jiǎn)單的部分。沒(méi)有任何東西表明這與在 Python 中編寫代碼有什么區(qū)別。這其中的一個(gè)有用方面是,如果你優(yōu)化了已經(jīng)有單元測(cè)試的 Python 中的現(xiàn)有庫(kù),你可以使用 Python 單元測(cè)試來(lái)測(cè)試 Rust 庫(kù)。
導(dǎo)入
無(wú)論你是使用 maturin develop
還是 pip install
來(lái)安裝它,導(dǎo)入庫(kù)都是使用 import
完成的。
import counter
構(gòu)造函數(shù)
構(gòu)造函數(shù)的定義正好使對(duì)象可以從 Python 構(gòu)建。這并不總是如此。有時(shí)僅從更復(fù)雜的函數(shù)返回對(duì)象。
cntr = counter.Counter(
'c',
3,
counter.Reset.NewlinesReset,
)
調(diào)用函數(shù)
最終的收益終于來(lái)了。檢查這個(gè)字符串是否至少有三個(gè) “c” 字符:
>>> cntr.has_count("hello-c-c-c-goodbye")
True
添加一個(gè)換行符會(huì)觸發(fā)剩余操作,這里沒(méi)有插入換行符的三個(gè) “c” 字符:
>>> cntr.has_count("hello-c-c-\nc-goodbye")
False
使用 Rust 和 Python 很容易
我的目標(biāo)是讓你相信將 Rust 和 Python 結(jié)合起來(lái)很簡(jiǎn)單。我編寫了一些“粘合劑”代碼。Rust 和 Python 具有互補(bǔ)的優(yōu)點(diǎn)和缺點(diǎn)。
Rust 非常適合高性能、安全的代碼。Rust 具有陡峭的學(xué)習(xí)曲線,對(duì)于快速原型解決方案而言可能有些笨拙。
Python 很容易入手,并支持非常緊密的迭代循環(huán)。Python 確實(shí)有一個(gè)“速度上限”。超過(guò)一定程度后,從 Python 中獲得更好的性能就更難了。
將它們結(jié)合起來(lái)完美無(wú)縫。在 Python 中進(jìn)行原型設(shè)計(jì),并將性能瓶頸移至 Rust 中。
使用 Maturin,你的開(kāi)發(fā)和部署流程更容易進(jìn)行。開(kāi)發(fā)、構(gòu)建并享受這一組合吧!