Python vs Rust:當不同開發哲學相遇
身為一位長期在兩個世界間穿梭的開發者,我常發現 Python 與 Rust 的對比就像是兩種截然不同的思考方式。Python 追求簡潔易讀,而 Rust 則致力於在編譯期就消除潛在問題。在協助多個團隊進行技術選型時,這種差異總是引發熱烈討論。
今天,我想從 Python 開發者的視角,帶你一窺 Rust 的世界。這不是一篇「哪種語言更好」的爭論文,而是希望透過比較,幫助你理解兩種語言各自的優勢與適用場景。
靜態類別:防患於未然的第一道防線
Python 與 Rust 最顯著的差異之一就是類別系統。Python 採用動態類別,讓開發者能快速原型設計;而 Rust 的靜態類別系統則在編譯階段就能發現潛在問題。
這種差異可以用一個簡單的比喻來說明:Python 像是騎單車時才發現問題;Rust 則像是在出門前就檢查單車的每個零件。
# Python:錯誤可能要等到執行時才被發現
def calculate_average(numbers):
total = 0
for num in numbers:
total += num
return total / len(numbers)
# 如果傳入空列表,會在執行時丟擲 ZeroDivisionError
而在 Rust 中,類別似的問題會在編譯時就被捕捉:
// Rust:編譯器會指出潛在的除零問題
fn calculate_average(numbers: &[f64]) -> f64 {
let total: f64 = numbers.iter().sum();
total / numbers.len() as f64 // 編譯器會警告可能的除零問題
}
有人可能會說:「Python 也有 mypy 這類別檢查工具啊!」沒錯,但讓我們看實際使用時的差異。
當 mypy 遇上 Rust 編譯器
Python 的 mypy 確實提供了靜態類別檢查功能,但它與 Rust 的編譯器檢查有著本質的差異。
以下是一個簡單例子:
from typing import List
def last(items: List[int]) -> int:
return items.pop()
在 mypy 的嚴格檢查下,這段程式碼不會報錯:
➜ mypy --strict types-01.py
Success: no issues found in 1 source file
看似一切正常,但這段程式碼存在一個潛在問題:如果 items
是空列表,.pop()
會丟擲 IndexError
異常。
讓我們看 Rust 中的類別似實作:
fn last(mut items: Vec<i32>) -> i32 {
items.pop()
}
嘗試編譯這段程式碼:
error[E0308]: mismatched types
|
1 | fn last(mut items: Vec<i32>) -> i32 {
| items.pop()
| ^^^^^^^^^^^ expected `i32`,
| found enum `std::option::Option`
|
= note: expected type `i32`
found enum `std::option::Option<i32>`
Rust 編譯器立即指出了問題:pop()
方法傳回的是 Option<i32>
而非 i32
,因為向量可能為空。這迫使開發者處理這種邊界情況。
為什麼 mypy 沒有捕捉到這個問題?因為 Python 的類別註解系統並不反映異常情況。在 Python 中,異常是控制流的一部分,而不是類別系統的一部分。
改進 Python 程式碼:向 Rust 學習
我們可以嘗試使用 Rust 的思維來改進 Python 程式碼:
from typing import List, Optional
def last(array: List[int]) -> Optional[int]:
if len(array) == 0:
return None
return array.pop()
這樣的程式碼更安全,但也更冗長,失去了 Python 的簡潔性。而與,這種避免異常的方法在 Python 中顯得有些不自然,因為例外處理是 Python 的核心設計哲學之一。
有些函式庫returns
嘗試在 Python 中引入 Rust 風格的錯誤處理,但這些解決方案往往感覺像是在 Python 中強行引入外來概念。
自訂類別與多型
在複雜系統開發中,自訂類別和多型是不可或缺的工具。讓我們看 Python 與 Rust 在這方面的差異。
Python 的靈活與 Rust 的安全
Python 中建立自訂類別非常直觀:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def greet(self) -> str:
return f"Hello, my name is {self.name}"
這種簡潔性使得 Python 非常適合快速原型設計。但這種靈活性也帶來了風險 - 你可以在任何時候修改物件的屬性,甚至可以動態增加新屬性。
Rust 的結構體(struct)則提供了更嚴格的保證:
struct User {
name: String,
age: u32,
}
impl User {
fn new(name: String, age: u32) -> Self {
User { name, age }
}
fn greet(&self) -> String {
format!("Hello, my name is {}", self.name)
}
}
在 Rust 中,一旦定義了結構體,其欄位就是固定的。這種嚴謹性在大型專案中特別有價值,因為它防止了意外修改資料結構的可能性。
列舉類別:Rust 的強大武器
Rust 的列舉類別(enum)遠比 Python 的對應物更強大。它們不僅可以列舉值,還可以攜帶資料:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
這種設計允許我們在一個類別中表達多種相關但不同的情況,每種情況可以攜帶不同的資料。
相比之下,Python 的列舉(透過 enum
模組)功能相對有限:
from enum import Enum
class MessageType(Enum):
QUIT = 0
MOVE = 1
WRITE = 2
CHANGE_COLOR = 3
# 但要攜帶資料,我們需要額外的結構
class Message:
def __init__(self, type: MessageType, data=None):
self.type = type
self.data = data
Option 與 Result:Rust 的錯誤處理哲學
Rust 的 Option
和 Result
類別是其錯誤處理哲學的核心。這兩個類別分別處理「可能不存在值」和「可能出錯的操作」。
Option:處理可能不存在的值
fn find_user(id: u64) -> Option<User> {
if id == 0 {
Some(User::new("Admin".to_string(), 30))
} else {
None
}
}
// 使用時必須處理 None 情況
match find_user(1) {
Some(user) => println!("Found user: {}", user.name),
None => println!("User not found"),
}
Result:處理可能失敗的操作
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(a / b)
}
}
// 使用時必須處理錯誤情況
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
這種設計迫使開發者明確處理所有可能的情況,大減少了執行時錯誤的可能性。
在 Python 中,我們通常依賴於例外處理或傳回特殊值(如 None
):
def divide(a: float, b: float) -> float:
if b == 0.0:
raise ValueError("Cannot divide by zero")
return a / b
# 使用時需要捕捉異常
try:
result = divide(10.0, 0.0)
print(f"Result: {result}")
except ValueError as e:
print(f"Error: {e}")
這種方法雖然有效,但容易導致遺漏錯誤處理,特別是當函式可能丟擲多種不同的異常時。
模式比對:優雅處理複雜資料
Rust 的模式比對是我認為最優雅的特性之一。它允許開發者以清晰、簡潔的方式處理複雜的資料結構和條件邏輯。
enum Action {
Move(i32, i32),
Speak(String),
ChangeColor(u8, u8, u8),
}
fn process_action(action: Action) {
match action {
Action::Move(x, y) => println!("Moving to ({}, {})", x, y),
Action::Speak(message) if message.len() > 0 => println!("Saying: {}", message),
Action::Speak(_) => println!("Nothing to say"),
Action::ChangeColor(r, g, b) => println!("Changing color to RGB({}, {}, {})", r, g, b),
}
}
Python 3.10 引入了模式比對功能,這是向 Rust 這類別語言學習的一個例子:
def process_action(action):
match action:
case ("move", x, y):
print(f"Moving to ({x}, {y})")
case ("speak", message) if message:
print(f"Saying: {message}")
case ("speak", _):
print("Nothing to say")
case ("change_color", r, g, b):
print(f"Changing color to RGB({r}, {g}, {b})")
case _:
print("Unknown action")
雖然 Python 的模式比對已經非常強大,但 Rust 的模式比對與其類別系統緊密結合,提供了更強大的安全保證。
特徵與協定:多型的不同途徑
Rust 的特徵(trait)和 Python 的協定(protocol)都是實作多型的機制,但它們的設計哲學有著顯著差異。
Python 的鴨子類別與協定
Python 以其「鴨子類別」聞名:如果它走路像鴨子,叫聲像鴨子,那麼它就是鴨子。這種思維體現在 Python 的協定中:
from typing import Protocol
class Serializable(Protocol):
def serialize(self) -> str:
...
class User:
def __init__(self, name: str):
self.name = name
def serialize(self) -> str:
return f"User:{self.name}"
# User 實作了 Serializable 協定,無需顯式宣告
def save_to_file(obj: Serializable, filename: str) -> None:
with open(filename, 'w') as f:
f.write(obj.serialize())
Rust 的特徵系統
Rust 的特徵系統則要求明確的實作宣告:
trait Serializable {
fn serialize(&self) -> String;
}
struct User {
name: String,
}
impl Serializable for User {
fn serialize(&self) -> String {
format!("User:{}", self.name)
}
}
fn save_to_file(obj: &impl Serializable, filename: &str) -> std::io::Result<()> {
std::fs::write(filename, obj.serialize())
}
Rust 的這種顯式宣告增加了程式碼的明確性,並允許編譯器進行更嚴格的檢查。
泛型程式設計:靜態與動態的權衡
泛型程式設計是現代語言的重要特性,但 Python 和 Rust 的實作方式有著本質差異。
Python 的動態泛型
Python 的泛型依賴於執行時的動態類別檢查:
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self):
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
# 建立字元串堆積積疊
stack = Stack[str]()
stack.push("hello") # 正
## 型別系統:程式碼的堅實基礎
在我的軟體開發生涯中,我逐漸認識到強大的型別系統不僅能防止錯誤,更是設計清晰、優雅程式碼的基礎。Rust的型別系統特別引人注目,它不只是防止錯誤的工具,更是構建穩健程式的核心元素。
今天讓我們探討Rust如何透過使用者自定義型別和多型來建立直觀與安全的程式碼,並且Python進行對比,以展示兩種不同型別系統的優缺點。
### 單位轉換問題:距離、時間與速度
假設我們面臨一個實際問題:需要處理不同單位的距離(公里、公尺)和時間(小時、秒),並計算相應的速度。這是一個絕佳的例子,展示如何使用自定義型別來確保計算的正確性和程式碼的可讀性。
## Rust中的單位型別設計
首先,讓我們在Rust中定義這些基本單位:
```rust
/// 距離,公里
struct Kilometer(f64);
/// 距離,公尺
struct Meter(f64);
/// 時間,小時
struct Hour(f64);
/// 時間,秒
struct Second(f64);
/// 速度,公里/小時
struct KmPerHour(f64);
/// 速度,公里/秒
struct KmPerSecond(f64);
/// 速度,公尺/小時
struct MeterPerHour(f64);
/// 速度,公尺/秒
struct MeterPerSecond(f64);
這種方法稱為「新型別模式」(newtype pattern),我在許多專案中發現這是Rust中一種極其有用的設計模式。它透過建立單欄位結構體,為基本型別提供了新的語義,同時保留了基礎型別的所有功能。
實作除法運算
接下來,我們需要實作不同單位間的除法運算。在Rust中,我們可以為自定義型別實作標準運算元:
use std::ops::Div;
/// 速度,公里/小時
impl Div<Hour> for Kilometer {
type Output = KmPerHour;
fn div(self, rhs: Hour) -> Self::Output {
KmPerHour(self.0 / rhs.0)
}
}
/// 速度,公里/秒
impl Div<Second> for Kilometer {
type Output = KmPerSecond;
fn div(self, rhs: Second) -> Self::Output {
KmPerSecond(self.0 / rhs.0)
}
}
/// 速度,公尺/小時
impl Div<Hour> for Meter {
type Output = MeterPerHour;
fn div(self, rhs: Hour) -> Self::Output {
MeterPerHour(self.0 / rhs.0)
}
}
/// 速度,公尺/秒
impl Div<Second> for Meter {
type Output = MeterPerSecond;
fn div(self, rhs: Second) -> Self::Output {
MeterPerSecond(self.0 / rhs.0)
}
}
這段程式碼展示了Rust的一個強大特性:為不同型別組合實作不同的運算邏輯。這裡,根據除數和被除數的不同型別,除法運算會產生不同型別的結果。
驗證實作
現在來測試我們的實作是否如預期工作:
fn main() {
let distance = Meter(100.);
let duration = Second(50.);
let speed = distance / duration; // MeterPerSecond
assert_eq!(speed.0, 2.);
let distance = Kilometer(180.);
let duration = Hour(3.);
let speed = distance / duration; // KmPerHour
assert_eq!(speed.0, 60.);
}
這段程式碼展示了Rust編譯器如何根據參與運算的型別,自動選擇正確的實作。編譯器在編譯時就能確保型別安全,避免單位混淆的錯誤。
Python中的型別提示
為了比較,讓我們看在Python中如何實作類別似的功能。Python 3.5引入了型別提示,使我們能夠標註變數和函式的預期型別。
首先定義我們的資料類別:
from dataclasses import dataclass
@dataclass
class Hour:
"""時間,小時。"""
value: float
@dataclass
class Second:
"""時間,秒。"""
value: float
@dataclass
class KmPerHour:
"""速度,公里/小時。"""
value: float
@dataclass
class KmPerSecond:
"""速度,公里/秒。"""
value: float
@dataclass
class MeterPerHour:
"""速度,公尺/小時。"""
value: float
@dataclass
class MeterPerSecond:
"""速度,公尺/秒。"""
value: float
Python中的多型實作
接下來,我們實作公里類別的除法運算,並使用typing.overload
來指定不同引數型別對應的不同傳回型別:
from typing import overload, Union
@dataclass
class Kilometer:
value: float
@overload
def __truediv__(self, other: Hour) -> KmPerHour: ...
@overload
def __truediv__(self, other: Second) -> KmPerSecond: ...
def __truediv__(self,
other: Union[Hour, Second]
) -> Union[KmPerHour, KmPerSecond]:
if isinstance(other, Hour):
return KmPerHour(self.value / other.value)
elif isinstance(other, Second):
return KmPerSecond(self.value / other.value)
這段程式碼使用了Python的型別提示系統,試圖模擬Rust的多型行為。但這裡有一個關鍵差異:Python的型別檢查只在開發時由工具如mypy執行,而非在執行時強制執行。
Python型別檢查的限制
讓我們看mypy如何檢查我們的程式碼:
➜ 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
看起來沒有問題。但如果我們故意引入一個錯誤,將除以秒數時傳回公里/小時而非公里/秒:
if isinstance(other, Hour):
return KmPerHour(self.value / other.value)
elif isinstance(other, Second):
return KmPerHour(self.value / other.value) # 應該傳回KmPerSecond
再次執行mypy:
➜ 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
mypy仍然沒有發現問題!這是因為我們的傳回型別被定義為Union[KmPerHour, KmPerSecond]
,所以傳回任一型別都被視為合法。
即使我們嘗試更明確地指定期望型別,mypy仍然無法捕捉這個錯誤:
speed: KmPerSecond = Kilometer(1.0) / Second(1.0)
assert isinstance(speed, KmPerHour) # 這應該是錯誤的
➜ 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
這突顯了Python型別系統的一個根本限制:它主要是為了檔案和開發時檢查而設計,無法提供Rust那樣的編譯時保證。
Rust的列舉型別(Enum)
Rust的另一個強大特性是其列舉型別,它遠比大多數語言中的列舉更強大。讓我們比較Python和Rust中的列舉實作。
Python中的列舉
在Python中,我們可以使用標準函式庫enum`模組:
from enum import Enum, auto
class UserStatus(Enum):
PENDING = auto()
ACTIVE = auto()
INACTIVE = auto()
DELETED = auto()
Rust中的基本列舉
Rust中的基本列舉看起來類別似:
enum UserStatus {
Pending,
Active,
Inactive,
Deleted,
}
Rust列舉的進階特性
但Rust的列舉遠比這強大。我們可以為每個變體關聯不同的資料:
use chrono::{DateTime, Utc};
enum UserStatus {
Pending(DateTime<Utc>), // 關聯等待時間
Active(i32), // 關聯使用者ID
Inactive(i32), // 關聯使用者ID
Deleted, // 無關聯資料
}
這種能力讓Rust的列舉成為一個強大的代數資料型別(algebraic data type)工具,可以表示更複雜的領域模型。每個變體可以攜帶不同型別和數量的資料,這在實作狀態機或處理可能有多種形式的資料時特別有用。
在我的專案中,我經常使用這種方式來模組化API回應、狀態轉換和錯誤處理,它讓程式碼更加直觀與安全。
型別系統比較的思考
經過這些比較,我得出幾點關於Rust和Python型別系統的思考:
編譯時vs執行時檢查:Rust的型別檢查發生在編譯時,能在程式執行前捕捉更多錯誤;Python的型別提示主要用於檔案和開發工具,無法提供相同的保證。
表達能力:Rust的型別系統允許更精確地表達程式邏輯和約束,特別是透過traits和泛型。
學習曲線:Rust的型別系統更為複雜,學習曲線較陡,但回報也更高。
靈活性與安全性的平衡:Python提供更大的靈活性但安全性較低;Rust則提供更高的安全性,代價是一些靈活性的損失。
多年來,我在不同專案中使用這兩種語言,發現它們各有適用場景。對於關鍵系統和效能敏感的應用,Rust的型別系統提供了無與倫比的安全保證;而對於快速原型開發和Script任務,Python的靈活性仍然非常有價值。
Rust的使用者自定義型別和多型能力使其成為建構複雜系統的絕佳工具,特別是在正確性至關重要的場合。這些特性不僅是防止錯誤的手段,更是構建清晰、可維護程式碼的強大工具。
在下一篇文章中,我們將探討Rust的模式比對(pattern matching)功能,這是與列舉型別緊密相關的另一個強大特性。
Rust的型別系統確實為我們提供了一種強大的思考和設計程式的方式,它不只是檢查錯誤,而是幫助我們更清晰地表達程式邏輯和意圖。我相信,隨著軟體系統複雜度不斷增加,這種強型別系統的價值將越發明顯。
Option 和 Result 型別
我們之前已經接觸過 Option
型別,當我們從數字向量中取出最右邊的元素時使用過。Result
與 Option
相似,但它可以包含兩種型別而非一種:操作的成功結果或錯誤。
pub enum Option<T> {
None,
Some(T),
}
pub enum Result<T, E> {
Ok(T),
Err(E),
}
讓我們透過一個例子來瞭解 Option
如何影回應用程式的正確性。
let mut vec = vec![1, 2, 3];
let last_element = vec.pop();
assert_eq!(last_element, Some(3));
當我們從向量中取出最右邊的元素時,我們得到的不是數字本身,而是包含在 Some
變體中的 Option
型別值。我們不能直接將它與另一個數字相加,因為這樣會丟失關於可能的 None
變體的資訊。
let mut vec = vec![1, 2, 3];
let last_element = vec.pop();
assert_eq!(last_element, Some(3));
let four = last_element + 1;
// 錯誤:不能將 `std::option::Option<{integer}>` 與 `{integer}` 相加
要使用從向量中取得的數字,我們可以使用模式比對,稍後會詳細討論。現在讓我們看在 Python 中類別似的程式碼如何運作。我們使用自己編寫的 last()
函式,使傳回型別為 Optional
。
from typing import List, Optional
def last(array: List[int]) -> Optional[int]:
if len(array) == 0:
return None
return array.pop()
numbers = [1, 2, 3]
last_element = last(numbers)
four = last_element + 1
➜ 01-types poetry run mypy --strict typing-04-1.py
typing-04-1.py:12: error: Unsupported operand types for + ("None" and "int")
typing-04-1.py:12: note: Left operand is of type "Optional[int]"
Mypy 與 Rust 編譯器一樣,不允許我們將可選值與數字相加。不過,為此程式設計師需要自行指定傳回值為 Optional
。
模式比對
既然我們提到了模式比對(pattern-matching),讓我們更詳細地探討這個概念。
首先看一下以下 Python 程式碼:
class UserStatus(Enum):
PENDING = auto()
ACTIVE = auto()
INACTIVE = auto()
DELETED = auto()
def serialize(user_status: UserStatus) -> str:
if user_status == UserStatus.PENDING:
return 'Pending'
elif user_status == UserStatus.ACTIVE:
return 'Active'
elif user_status == UserStatus.INACTIVE:
return 'Inactive'
elif user_status == UserStatus.DELETED:
return 'Deleted'
這段程式碼的功能很簡單——將 UserStatus 列舉的元素轉換為字串表示。看起來相當直觀。
現在看 Rust 中的對應版本:
enum UserStatus {
Pending,
Active,
Inactive,
Deleted,
}
fn serialize(user_status: UserStatus) -> &'static str {
match user_status {
UserStatus::Pending => "Pending",
UserStatus::Active => "Active",
UserStatus::Inactive => "Inactive",
UserStatus::Deleted => "Deleted",
}
}
兩者的關鍵差異在於,如果開發者因某種原因(例如重構時新增了使用者狀態)忘記在 serialize 函式中描述列舉的某個變體,Rust 會明確提示:
fn serialize(user_status: UserStatus) -> &'static str {
match user_status {
UserStatus::Pending => "Pending",
UserStatus::Active => "Active",
}
}
// 錯誤:非窮盡的模式:未覆寫 `Inactive` 和 `Deleted`
這正是 Rust 中模式比對的一個顯著特性。使用模式比對時,編譯器會強制我們考慮所有可能的情況。
回到我們之前提到的 last
函式:當處理函式呼叫結果的 Option 時,編譯器不會讓我們忘記處理結果為 None 的情況。
同樣的規則也適用於 Result
型別:
let number = "5";
let parsed: Result<i32, ParseIntError> = number.parse();
let message = match parsed {
Ok(value) => format!("Number parsed successfully: {}", value),
Err(error) => format!("Can't parse a number. Error: {}", error),
};
assert_eq!(message, "Number parsed successfully: 5");
如果我們需要定義某種預設行為,Rust 提供了以下結構:
fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
在這個例子中,我們只描述了兩個特定值,對於所有其他值,則遞迴呼叫 fibonacci
函式。
特徵與協定
在這一節中,我們將比較 Python 中最近引入的協定(protocols)與 Rust 的特徵(traits)。可能不是所有人都使用協定,為了使比較更有意義,我們先簡要概述協定的基本概念。
假設我們需要編寫一個驗證器函式,接收 Image
類別例項的列表,並傳回布林值列表。True
表示圖片有效與大小不超過 MAX_SIZE
,False
表示無效。來寫程式碼:
from typing import List
MAX_SIZE = 512_000
class Image:
def __init__(self, image: bytes) -> None:
self.image = image
def validate(images: List[Image]) -> List[bool]:
return [len(image) <= MAX_SIZE for image in images]
__CODE_BLOCK_42__bash
➜ 01-types poetry run mypy --strict p-01-2.py
p-01-2.py:8: error: Argument 1 to "len" has incompatible type "Image"; expected "Sized"
Found 1 error in 1 file (checked 1 source file)
__CODE_BLOCK_43__python
from typing import List
MAX_SIZE = 512_000
class Image:
def __init__(self, image: bytes) -> None:
self.image = image
def __len__(self) -> int:
return len(self.image)
def validate(images: List[Image]) -> List[bool]:
return [len(image) <= MAX_SIZE for image in images]
__CODE_BLOCK_44__python
from abc import abstractmethod
from typing import List, Protocol
class SupportsReview(Protocol):
@abstractmethod
def approved(self) -> bool: ...
class Article(SupportsReview):
def approved(self) -> bool:
return True
class PhotoGallery(SupportsReview):
def approved(self) -> bool:
return True
class Test(SupportsReview):
def approved(self) -> bool:
return True
def validate(documents: List[SupportsReview]) -> List[SupportsReview]:
return [
document for document in documents
if document.approved()
]
documents = [Article(), PhotoGallery(), Test()]
approved_documents = validate(documents)
assert len(approved_documents) == 3
在這段程式碼中,我們定義了 SupportsReview
協定,而驗證器會處理所有實作此協定的類別。如果其中一個類別沒有支援 SupportsReview
,mypy 會提示 documents
中存在不相符的型別。
Python 協定與 Rust 特徵的比較
比較 Python 中的協定與 Rust 中的特徵(trait),我們會發現它們非常相似。讓我們用 Rust 實作相同的功能:
首先,建立 Review
特徵:
trait Review {
fn approved(&self) -> bool;
}
接著,建立結構體並為它們實作 Review
特徵:
struct Article;
impl Review for Article {
fn approved(&self) -> bool {
true
}
}
struct PhotoGallery;
impl Review for PhotoGallery {
fn approved(&self) -> bool {
true
}
}
struct Test;
impl Review for Test {
fn approved(&self) -> bool {
true
}
}
最後,定義 validate
函式並執行程式碼:
fn validate(documents: Vec<Box<dyn Review>>) -> Vec<Box<dyn Review>> {
documents
.into_iter()
.filter(|document| document.approved())
.collect::<Vec<_>>()
}
fn main() {
let documents: Vec<Box<dyn Review>> = vec![
Box::new(Article),
Box::new(PhotoGallery),
Box::new(Test),
];
let approved = validate(documents);
assert_eq!(approved.len(), 3);
}
Rust 版本的程式碼看起來比 Python 版本複雜一些,主要是因為出現了 Box
型別以及使用 dyn Review
來描述特徵支援。這是靜態型別系統所帶來的成本,也是靜態型別與動態型別間的權衡。
泛型程式設計
我們已經討論了協定,並瞭解到可以透過協定對使用的型別施加限制。但如果我們需要為型別描述多個限制,並指定在所有地方都使用相同的型別,該怎麼辦呢?這時泛型(generics)就能派上用場。讓我們看 Python 和 Rust 如何處理泛型。
首先,實作一個二元搜尋樹的節點:
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class Node(Generic[T]):
def __init__(self, value: T,
left: Optional['Node'[T]] = None,
right: Optional['Node'[T]] = None,
) -> None:
self.value = value
self.left = left
self.right = right
if __name__ == '__main__':
root = Node(2)
root.left = Node(1)
root.right = Node(3)
我們定義了一個泛型別 T
,它可以儲存在節點內部。執行 mypy 檢查,確認所有內容都正確描述:
➜ 01-types poetry run mypy --strict generics-01-1.py
Success: no issues found in 1 source file
如果我們在節點中使用錯誤的值類別,看 mypy 如何捕捉到這個錯誤:
root = Node(2)
root.left = Node(1)
root.right = Node('Hello!') # 這裡有錯誤
當建立樹的根節點時,mypy 將 T
型別確定為 int
,因此不允許我們使用 str
型別建立其他節點:
generics-01-1.py:18: error: Argument 1 to "Node" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)
mypy 正確地捕捉到了這個錯誤。
不過,目前的節點定義是否足夠?我們只加了一個限制 - 樹中所有型別必須相同。但要實作二元搜尋樹,我們需要能夠比較節點內的值。例如,目前我們可以在節點中放入 None
,與程式碼仍被視為正確。
讓我們為型別 T
增加額外限制 - T
必須實作比較協定。我們來尋找 Comparable
協定。
可惜的是,關於這個協定的討論早在 2015 年就開始了,但它至今仍未出現。因此,我們自行實作:
C = TypeVar('C')
class Comparable(Protocol):
def __lt__(self: C, other: C) -> bool: ...
def __gt__(self: C, other: C) -> bool: ...
def __le__(self: C, other: C) -> bool: ...
def __ge__(self: C, other: C) -> bool: ...
然後將它加入到二元搜尋樹中:
...
T = TypeVar('T', bound=Comparable)
class Node(Generic[T]):
def __init__(self, value: T,
left: Optional['Node'[T]] = None,
right: Optional['Node'[T]] = None,
) -> None:
self.value = value
self.left = left
self.right = right
def add(self, node: 'Node'[T]) -> None:
if node.value <= self.value:
self.left = node
else:
self.right = node
if __name__ == '__main__':
root = Node(2)
root.add(Node(1))
root.add(Node(3))
現在,我們的節點只接受實作了 Comparable
協定的型別,這樣就能確保節點之間可以進行比較操作,從而正確實作二元搜尋樹的邏輯。
靜態型別的實戰對比:Rust與Python的泛型與多執行緒安全性
在高階程式語言的世界裡,型別系統扮演著至關重要的角色。作為一位長期在這兩種語言間切換的開發者,我發現Rust和Python在型別處理上的差異不僅是語法層面,更深入到語言核心設計哲學。今天就讓我們透過實際案例比較這兩種語言的型別系統特性,特別關注泛型實作和多執行緒安全性這兩個關鍵方面。
Python的型別檢查:mypy的功能與限制
Python原生是動態型別語言,但透過mypy等工具,我們可以實作某種程度的靜態型別檢查。讓我們先看一個使用mypy檢查的例子:
root = Node(None)
root.add(Node(None))
root.add(Node(None))
執行mypy檢查時,我們會得到以下錯誤訊息:
➜ 01-types poetry run mypy --strict generics-01-4.py
generics-01-4.py:35: error: Value of type variable "T" of "Node" cannot be "None"
generics-01-4.py:36: error: Value of type variable "T" of "Node" cannot be "None"
generics-01-4.py:37: error: Value of type variable "T" of "Node" cannot be "None"
Found 3 errors in 1 file (checked 1 source file)
mypy成功捕捉到了型別錯誤,這很好。但讓我們看同樣的概念如何在Rust中實作。
Rust的泛型實作
在Rust中,同樣的樹節點結構可以這樣實作:
struct Node<T>
where T: Ord
{
pub value: T,
pub left: Option<Box<Node<T>>>,
pub right: Option<Box<Node<T>>>,
}
impl<T> Node<T>
where T: Ord
{
pub fn add(&mut self, node: Node<T>) {
if node.value <= self.value {
self.left = Some(Box::new(node))
} else {
self.right = Some(Box::new(node))
}
}
}
fn main() {
let mut root = Node { value: 2, left: None, right: None };
let node_1 = Node { value: 1, left: None, right: None };
let node_3 = Node { value: 3, left: None, right: None };
root.add(node_1);
root.add(node_3);
}
__CODE_BLOCK_57__python
from typing import TypeVar, Generic, Sized, Hashable
T = TypeVar('T', Hashable, Sized)
class Base(Generic[T]):
def __init__(self, bar: T):
self.bar: T = bar
class Child(Base[T]):
def __init__(self, bar: T):
super().__init__(bar)
__CODE_BLOCK_58__bash
➜ 01-types poetry run mypy --strict generics-01-6.py
generics-01-6.py:13: error: Argument 1 to "__init__"
of "Base" has incompatible type "Hashable"; expected "T"
generics-01-6.py:13: error: Argument 1 to "__init__"
of "Base" has incompatible type "Sized"; expected "T"
Found 2 errors in 1 file (checked 1 source file)
__CODE_BLOCK_59__python
import threading
x = 0
def increment_global():
global x
x += 1
def taskof_thread():
for _ in range(50000):
increment_global()
def main():
global x
x = 0
t1 = threading.Thread(target=taskof_thread)
t2 = threading.Thread(target=taskof_thread)
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i, x))
__CODE_BLOCK_60__python
$ python race.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 86114 after Iteration 2
x = 58422 after Iteration 3
x = 89266 after Iteration 4
__CODE_BLOCK_61__python
def taskof_thread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
__CODE_BLOCK_62__python
lock = threading.Lock()
t1 = threading.Thread(target=taskof_thread, args=(lock,))
t2 = threading.Thread(target=taskof_thread, args=(lock,))
__CODE_BLOCK_63__python
❯ python race.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
__CODE_BLOCK_64__rust
use std::thread;
static mut X: i32 = 0;
fn increment_global() {
X += 1;
}
fn thread_task() {
for _ in 0..50_000 {
increment_global()
}
}
fn main_task() {
let t1 = thread::spawn(thread_task);
let t2 = thread::spawn(thread_task);
t1.join().unwrap();
t2.join().unwrap();
}
fn main() {
for i in 0..5 {
main_task();
println!("x = {} after Iteration {}", X, i);
}
}
__CODE_BLOCK_65__rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
for i in 0..5 {
let counter = Arc::new(Mutex::new(0));
let counter1 = Arc::clone(&counter);
let t1 = thread::spawn(move || {
for _ in 0..50_000 {
let mut num = counter1.lock().unwrap();
*num += 1;
}
});
let counter2 = Arc::clone(&counter);
let t2 = thread::spawn(move || {
for _ in 0..50_000 {
let mut num = counter2.lock().unwrap();
*num += 1;
}
});
t1.join().unwrap();
t2.join().unwrap();
println!("x = {} after Iteration {}", *counter.lock().unwrap(), i);
}
}
__CODE_BLOCK_66__
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
__CODE_BLOCK_67__rust
use std::{sync::{Arc, Mutex}, thread};
use lazy_static::lazy_static; // 1.4.0
lazy_static! { static ref X: Mutex<i32> = Mutex::new(0); }
fn increment_global(x: &Mutex<i32>) {
let mut data = x.lock().unwrap();
*data += 1;
}
fn thread_task(x: &Mutex<i32>) {
for _ in 0..50_000 {
increment_global(x)
}
}
fn main_task(x: &'static Mutex<i32>) {
let mut threads = vec![];
for _ in 0..2 {
threads.push(thread::spawn(move || thread_task(x)));
}
for thread in threads {
thread.join().unwrap();
}
}
fn main() {
for i in 0..5 {
main_task(&X);
let mut data = X.lock().unwrap();
println!("x = {} after Iteration {}", data, i);
}
}
這段程式碼中,我們使用了 Mutex
來安全地包裝可變資料。在 Rust 中,Mutex
不僅是一個獨立的鎖,它實際上是資料的包裝器。這種設計有一個重要優勢:你必須透過鎖定 Mutex
才能存取其中的資料,這使得忘記鎖定或鎖定錯誤的互斥鎖變得幾乎不可能。
當我在一家金融科技公司開發高頻交易系統時,這種強制性的安全檢查幫助我們避免了許多潛在的並發問題。在 Python 中,即使用了 threading.Lock
,仍然可能忘記在存取分享資料前取得鎖,或者取得了錯誤的鎖。
Rust 與 Python 的多執行緒對比
Rust 的多執行緒安全保障主要體現在兩個方面:
- 編譯時檢查:Rust 在編譯階段就能檢測到潛在的資料競爭問題
- 類別系統整合:同步原語(如
Mutex
)與它們保護的資料緊密繫結
雖然 Python 的 threading.Lock
可以用作連貫的背景與環境管理器(with 陳述式),但它無法與被保護的資料關聯起來。這使得開發者必須手動確保在每次存取分享資料時都正確使用鎖,這容易出錯。
Rayon:簡化平行程式設計
Rust 生態系統中有許多優秀的平行程式函式庫中 rayon
是一個典型代表。它可以讓我們用極少的修改就能將順序程式碼轉換為平行程式碼:
順序版本:
fn main() {
let mut arr = [0, 7, 9, 11];
arr.iter_mut().for_each(|p| *p -= 1);
println!("{:?}", arr);
}
平行版本:
use rayon::prelude::*;
fn main() {
let mut arr = [0, 7, 9, 11];
arr.par_iter_mut().for_each(|p| *p -= 1);
println!("{:?}", arr);
}
只需將 iter_mut()
替換為 par_iter_mut()
,程式就能自動利用多核心進行平行計算,而與 Rust 的類別系統確保這種平行是安全的。
非同步程式設計
Rust 與 Python 在非同步程式設計方面有許多相似之處,尤其是在語法層面。以下是一個簡單的非同步程式範例對比:
Python 版本:
import asyncio
async def timers():
futures = []
for s in range(3):
futures.append(asyncio.sleep(s))
await asyncio.gather(*futures)
if __name__ == "__main__":
asyncio.run(timers())
Rust 版本:
use tokio::time::{delay_for, Duration};
use futures::future::join_all;
async fn timers() {
let futures = (0..2)
.map(Duration::from_secs)
.map(delay_for);
join_all(futures).await;
}
#[tokio::main]
async fn main() {
timers().await;
}
值得注意的是,Rust 的 async/await 語法是相對較新的功能,於 2019 年 11 月在 1.39.0 版本中才成為穩定特性。在此之前,Rust 的非同步程式需要使用組合子(combinator)風格:
fn main() {
let addr = "127.0.0.1:1234".parse().unwrap();
let future = TcpStream::connect(&addr)
.and_then(|socket| {
io::write_all(socket, b"hello world")
})
.and_then(|(socket, _)| {
// 讀取剛好 11 個位元組
io::read_exact(socket, vec![0; 11])
})
.and_then(|(socket, buf)| {
println!("got {:?}", buf);
Ok(())
})
.map_err(|_| println!("failed"));
tokio::run(future);
}
Rust 與 Python 在並發程式設計方面採取了不同的方法。Rust 透過其類別系統和所有權模型在編譯時提供強大的安全保證,幫助開發者避免常見的並發錯誤。在構建高效能、安全的多執行緒應用程式時,這些保證極為寶貴。
雖然 Rust 的學習曲線較陡,但一旦掌握了其並發模型,就能寫出既安全又高效的並發程式碼。在我多年的系統開發經驗中,這種編譯時的安全檢查為團隊節省了大量的除錯時間,特別是在處理複雜的並發問題時。
對於需要高效能和高可靠性的系統,Rust 的並發模型提供了一個強大的工具,使開發者能夠自信地構建複雜的並發應用而不必擔心常見的並發陷阱。
Rust與Python的非同步與函式程式設計對比
非同步模型差異
對於已經習慣Python async/await的開發者來說,Rust的非同步處理方式可能顯得相當陌生。然而,這種差異化設計帶來了獨特的優缺點,特別是對於熟悉函式語言的開發者而言,這種方式會感到相當親切。值得注意的是,在Rust中,你可以同時使用組合子和await語法,靈活結合兩種風格。與Python不同,Rust廣泛採用了通道(channels)概念進行非同步通訊。
兩種語言的相似之處在於都有多種事件迴圈(event loop)的實作。不過,兩者在編寫與特定事件迴圈函式庫的抽象程式碼時都存在一定挑戰。
函式程式設計正規化
讓玄貓探討這兩種語言的函式能力。雖然這個主題極為深厚,無法完全涵蓋,但玄貓會從實用角度進行分析。
高階函式
高階函式是Python和Rust都支援的基本函式概念。來比較兩種語言的實作方式:
from typing import List, Callable
def map_each(list: List[str], fun: Callable[[str], int]) -> List[int]:
new_array = []
for it in list:
new_array.append(fun(it))
return new_array
if __name__ == '__main__':
languages = [
"Python",
"Rust",
"Go",
"Haskell",
]
out = map_each(languages, lambda it: len(it))
print(out) # [6, 4, 2, 7]
在Python範例中,玄貓使用了typing模組來明確定義函式簽名。map_each
函式接收兩個引數:一個字串列表和一個可呼叫物件(Callable)。這個可呼叫物件接收字串並傳回數字,整體語法相當直觀。
對應的Rust實作:
fn map_each(list: Vec<String>, fun: fn(&String) -> usize) -> Vec<usize> {
let mut new_array: Vec<usize> = Vec::new();
for it in list.iter() {
new_array.push(fun(it));
}
return new_array;
}
fn main() {
let list = vec![
String::from("Python"),
String::from("Rust"),
String::from("Go"),
String::from("Haskell"),
];
let out = map_each(list, |it| it.len());
println!("{:?}", out); // [6, 4, 2, 7]
}
雖然看起來相似,但有一個關鍵差異:Python不支援多行lambda函式。這嚴重限制了函式程式設計的表達能力。當然,可以透過使用普通函式來規避這個限制,但這確實降低了使用體驗。
另一個重要區別在於"組合子"的實作原則。在Rust中,大多陣列合子是Iterator特徵(trait)的一部分,而Python則通常將它們實作為獨立函式。由於Iterator特徵在Rust的標準類別中廣泛實作,與可為自定義類別實作,這使得Rust能更自然地將方法連結成複雜的資料轉換管道。
fn main() {
let list = vec![
Ok(42),
Err("Error - 1"),
Ok(81),
Ok(88),
Err("Error - 2")
];
let out: Vec<_> = list
.iter()
.flatten()
.collect();
println!("{:?}", out); // [42, 81, 88]
}
上面的程式碼從向量中過濾出錯誤結果,並將剩餘元素轉換為整數向量。這種實作依賴於Result類別對IntoIterator特徵的支援。
再看一個展示Rust能力的例子:
fn main() {
let output = ["42", "43", "sorokchetire"]
.iter()
.map(|string| {
string
.parse::<i32>()
.map_err(|_| println!("Parsing error"))
})
.flatten()
.map(|integer| integer * 2)
.fold(0, |base, element| base + element);
println!("{:?}", output);
}
這段程式碼實作了更複雜的功能:迭代字串陣列,嘗試將每個元素轉換為i32,遇到錯誤時輸出錯誤資訊,然後過濾掉不正確的結果,將有效值乘以2,最後將所有元素相加。
這種鏈式呼叫的優勢在於可讀性和維護性。當需要在現有流程中增加新的轉換步驟時,只需插入額外的map呼叫即可。雖然Python確實有列表推導式(Comprehensions),但在其中執行複雜的資料操作並不那麼方便。
閉包概念在Python和Rust中都廣泛使用,但Rust的類別系統允許進行Python無法實作的某些檢查。首先看一個Rust的例子:
fn main() {
let mut holder = vec![];
let sum = |a: usize, b: usize| -> usize {
let c = a + b;
holder.push(a + b);
c
};
println!("{}", sum(10, 20));
}
這段程式碼定義了一個閉包sum
,它接收兩個引數並將它們相加。計算結果被儲存到向量holder
中並從閉包傳回。重要的是,holder
是閉包外部的變數,代表某種全域狀態。嘗試編譯這段程式碼會得到錯誤:
error[E0596]: cannot borrow `sum` as mutable, as it is not declared as mutable
編譯器禁止我們在閉包中修改外部狀態,除非閉包本身被宣告為mut
。修正方法很簡單:
fn main() {
let mut holder = vec![];
let mut sum = |a: usize, b: usize| -> usize {
let c = a + b;
holder.push(a + b);
c
};
println!("{}", sum(10, 20));
}
在玄貓的開發經驗中,這種編譯時的安全檢查極大地減少了執行時錯誤。Rust的類別系統和所有權模型不僅使程式碼更安全,還迫使開發者更清晰地思考資料流和變數可變性,這在構建大型複雜系統時特別有價值。
相比之下,Python的動態特性讓開發更加靈活,但也更容易引入難以追蹤的執行時錯誤。這種權衡在選擇技術堆積積疊時值得深思,尤其是針對需要長期維護的企業級應用。
無論選擇哪種語言,理解其函式程式設計能力都能顯著提高程式碼品質和開發效率。特別是在處理資料轉換和非同步操作時,函式風格往往能帶來更簡潔、更易於理解的解決方案。
函式純度與型別系統的邊界
在型別系統層面上完全區分純函式確實具有挑戰性,但Rust提供的機制能讓我們明確識別哪些閉包在呼叫時會明顯改變外部狀態,這已經是一個相當實用的進步。這種情況與Rust和Python在處理可變性和不可變性時的差異相似。
在Rust中,編譯器明確要求開發者指定哪些資料集是可變的,哪些不是。這種方法比Python中使用的方式更加靈活,因為Python中的可變性是由資料型別本身決定,基本上無法改變這一特性。
// Rust中明確標記可變性
let mut variable = 5; // 可變數
let constant = 10; // 不可變數
// 閉包中的可變借用需明確標記
let mut counter = 0;
let increment = |x| {
counter += x; // 明確修改外部狀態
counter
};
相較之下,Python無法在型別層級上區分這種可變性:
# Python中無法在型別層級區分可變性
counter = 0
def increment(x):
global counter
counter += x # 修改外部狀態,但型別系統無法識別
return counter
這個差異凸顯了Rust如何在設計層級上更嚴格地控制副作用,即使它還無法完全在型別系統中區分純函式。
遞迴處理:相似但有關鍵差異
遞迴是另一個兩種語言處理方式相似但有重要差異的概念。Rust和Python都缺乏預設的尾遞迴最佳化,但它們處理遞迴限制的方式有所不同。
Rust的LLVM後端實際上可以生成考慮尾遞迴最佳化的程式碼。這意味著將來Rust可能會在符合所有條件的情況下提供保證的尾遞迴最佳化。
// Rust中的遞迴函式
fn factorial(n: u64) -> u64 {
// 非尾遞迴版本
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
// 可以改寫為尾遞迴形式
fn factorial_tail(n: u64, acc: u64) -> u64 {
if n <= 1 {
acc
} else {
factorial_tail(n - 1, n * acc)
}
}
而Python則依賴於遞迴深度限制:
# Python中的遞迴
def factorial(n):
if n <= 1:
return 1
else:
return n * factorial(n - 1)
# Python可以捕捉遞迴限制異常
try:
result = factorial(10000) # 會觸發RecursionError
except RecursionError:
print("遞迴太深了,需要重新設計演算法")
兩種語言處理遞迴錯誤的方式也有差異。在Python中,RecursionLimit
是一個普通的例外,可以捕捉並允許程式繼續執行。而在Rust中,如果超過遞迴呼叫的巢狀層級,則可能發生堆積積疊溢位錯誤(stack overflow),這是一個更嚴重的執行時錯誤。
這個差異反映了兩種語言的設計理念:Python傾向於提供更多的執行時安全網,而Rust則更傾向於在編譯時強制開發者處理潛在問題。
為何Python開發者應該考慮Rust
經過對這些核心概念的分析,我們可以更清楚地看到為何Python開發者可能會從學習Rust中獲益。
Python開發者常需要手動處理許多事情:檢查型別、記住哪裡會有None、哪裡會有數字、在檔案中查詢函式可能拋出的例外。這種手動管理是複雜與容易出錯的,這也是我們前面討論過的大多數錯誤的來源。
雖然Mypy可以幫助消除部分這類別錯誤,但Rust編譯器在這方面的表現遠勝於Mypy。當然,Rust也有自己的陡峭學習曲線和潛在陷阱。
Rust確實有相對較高的入門檻。要有效使用它,開發者需要理解我們上面討論的概念以及更多我們尚未涉及的概念。有些概念確實很複雜。隨著時間的推移,你會開始理解這些概念,它們不再看起來那麼複雜,也不會大幅減慢開發過程。但這需要時間,這個門檻必須跨越。
在玄貓多年的跨語言開發經驗中,我觀察到一個明顯的模式:Python應用程式的開發速度確實很快,這使得它在快速原型設計和某些領域非常具有吸引力。然而,當需要維護這些應用程式時,開發期間忽略的問題就會浮現出來。
相比之下,Rust不允許開發者忽視可能的情況,它要求更多的開發時間,但正因如此,後期維護和重構所需的工作量大減少。這種前期投資換來的是長期的可維護性和穩定性。
這種權衡在選擇程式語言時非常關鍵。如果你的專案需要快速開發原型並且可以接受後期維護的挑戰,Python可能是更好的選擇。但如果你正在構建需要長期維護、高效能與可靠的系統,那麼投資學習Rust可能會帶來更大的長期回報。
對於經驗豐富的Python開發者來說,學習Rust不僅是掌握一門新語言,更是獲得一種新的思考方式,這種思考方式可以幫助你寫出更安全、更可維護的程式碼,無論你使用的是哪種語言。
在我的職業生涯中,我發現掌握多種正規化的程式語言大提升了我解決問題的能力。Rust帶來的所有權和借用概念,即使在使用Python時也能影響我的設計決策,使我能夠寫出更清晰、更少錯誤的程式碼。
從Python到Rust的轉變可能具有挑戰性,但這種挑戰帶來的技術成長和對程式設計更深入的理解是值得的。不論你最終選擇哪種語言作為主要工具,理解這兩種語言的優勢和限制都會使你成為一個更全面的開發者。