面向 Python 开发者的 Rust:实用指南
如果你会 Python,Rust 的很多想法你其实已经懂了。Rust 有遍历迭代器的 for 循环、元组、闭包(相当于 Python 的 lambda,但更好用)、和 Python 的 match 很像的 match 语句,还有能干列表推导式那份活的迭代器链。Rust 甚至借鉴了 Python 的一些品味:格式化代码只有一种显而易见的方式,包管理工具很友好,错误信息也在努力帮你。
新东西主要来自编译器。Python 在程序运行时检查类型;Rust 在运行之前就把类型全部检查一遍。Python 用垃圾回收器清理内存;Rust 用所有权和借用,在编译时检查。Python 有 None 和异常;Rust 有 Option 和 Result。你的大部分学习时间都会花在这三点上。
那 Python 开发者为什么要学 Rust?说实话,理由是这几条:
- 速度。 Rust 编译成原生机器码。在纯 Python 里很慢的 CPU 密集型工作(解析、数值计算、大循环),用 Rust 写通常会快很多,而且不用什么调优技巧。
- 真正的线程。 在标准的 CPython 里,GIL 让同一时间只有一个线程能运行 Python 代码。Rust 的线程能同时跑满你所有的 CPU 核心,而且编译器会在程序运行之前就阻止数据竞争。
- 更早发现 bug。 属性名写错了、忘了处理某个
None、调用函数时传错了类型:在 Python 里,要等那一行真正运行时你才会发现。在 Rust 里,这些问题不修好,程序就编译不过。 - 不用二选一。 有了 PyO3(英文)和 maturin,你可以用 Rust 写 Python 项目里慢的那一部分,再像普通模块一样在 Python 里
import它。Polars、Ruff、uv 这些流行的 Python 工具就是用 Rust 写的。
如果你想看 Rust 和 Python、Go、C++ 的整体对比,请看 Rust 与 C++、Go、Python 对比。
语言概念:从 Python 到 Rust
Section titled “语言概念:从 Python 到 Rust”把这张表当成一本“短语手册”来用。最后一列链接到本站讲解 Rust 这一边的课程。
| Python | Rust | 延伸学习 |
|---|---|---|
| 动态类型,运行时检查 | 静态类型,编译时检查。大部分类型 Rust 都能推断出来,所以你要写的类型比想象中少 | Rust 数据类型 |
类型提示(def f(x: int) -> str) |
函数签名里的类型是必须写的,而且由编译器强制检查 | Rust 函数 |
x = 5 |
let x = 5;(不能修改)。想修改就写 let mut x = 5; |
Rust 变量 |
int(要多大有多大) |
固定大小的整数:i32、i64、u64、usize、i128……它们可能溢出 |
Rust 数据类型、Rust 整数溢出 |
float |
f64(或 f32) |
Rust 数据类型 |
str |
String(拥有所有权、可增长)和 &str(借用的文本视图) |
Rust 字符串 |
f-string:f"Hi {name}" |
format!("Hi {name}") 和 println!("Hi {name}") |
Rust 输出 |
print(repr(x)) / __repr__ |
加上 #[derive(Debug)],然后用 println!("{x:?}") |
Rust 输出、常用 trait |
__str__ |
实现 Display(然后 .to_string() 也能用了) |
常用 trait |
__eq__、__hash__、__lt__ |
派生 PartialEq、Eq、Hash、PartialOrd、Ord |
常用 trait |
None、Optional[T] |
Option<T>:要么是 Some(value),要么是 None |
Rust Option |
x or default |
x.unwrap_or(default) |
Rust Option |
raise、try/except |
Result<T, E> 加 ? 运算符 |
Rust Result、自定义错误类型 |
表示真正 bug 的异常(assert、IndexError) |
panic!(终止当前线程) |
Rust panic |
带 __init__ 和方法的 class |
用 struct 存数据,再用 impl 块写方法;new 只是一个普通函数 |
Rust 结构体 |
@dataclass |
加上 #[derive(Debug, Clone, PartialEq)] 的 struct |
Rust 结构体 |
self |
&self(只读)、&mut self(修改)或 self(拿走所有权) |
Rust 结构体 |
| 继承 | 没有继承。用组合(结构体里套结构体)加带默认方法的 trait | Rust trait、设计模式 |
鸭子类型、Protocol、ABC |
trait |
Rust trait |
| 装着不同类的对象的列表 | Vec<Box<dyn Trait>>(trait 对象) |
trait 对象(dyn) |
TypeVar / 泛型函数 |
带 trait 约束的泛型:fn largest<T: Ord>(items: &[T]) |
Rust 泛型 |
Enum 类 |
每个变体都能携带自己数据的 enum |
Rust 枚举 |
match / case(3.10+) |
match(编译器会检查你是否覆盖了所有情况) |
Rust match、Rust 模式匹配 |
if my_list:(真值判断) |
要写清楚:if !my_list.is_empty()。if 里只能放 bool |
Rust if…else |
for i in range(10) |
for i in 0..10 |
Rust for 循环 |
enumerate(xs)、zip(a, b) |
xs.iter().enumerate()、a.iter().zip(b.iter()) |
Rust 迭代器 |
lambda x: x + 1 |
闭包:|x| x + 1(而且可以写很多行) |
Rust 闭包 |
列表推导式、map、filter |
迭代器适配器:filter、map,最后 collect() |
Rust 迭代器 |
生成器和 yield |
自己写一个实现了 Iterator 的类型,或者用 std::iter::from_fn |
自定义迭代器 |
with open(...) as f: |
什么都不用写:所有者离开作用域时,文件和锁会自动关闭(Drop) |
Rust 作用域、文件读写 |
默认参数、*args、**kwargs |
不支持。用 Option 参数、slice(&[T])、实现了 Default 的结构体,或者 builder |
Rust 函数、设计模式 |
| 装饰器 | 没有直接对应的东西。用闭包包一层,或者用属性宏(比如 #[tokio::main]) |
Rust 闭包、过程宏 |
import 和模块 |
mod 和 use |
Rust 模块 |
| PyPI 上的包 | crates.io 上的 crate | Cargo 与 crate |
if __name__ == "__main__": |
fn main() |
Rust 快速上手 |
threading(受 GIL 限制) |
std::thread:真正并行的线程 |
Rust 并发 |
multiprocessing、concurrent.futures |
线程,或者用 rayon crate 的并行迭代器 |
Rust 并发、共享状态 |
asyncio、async def、await |
async fn 和 .await,配合 tokio 这样的运行时 |
async/await |
7 // 2、2 ** 10 |
7 / 2(整数相除就是整数除法)、2_i64.pow(10) |
Rust 运算符 |
int("42")、str(42) |
"42".parse::<i32>()(返回 Result)、42.to_string() |
Rust 类型转换、From 与 Into 转换 |
Python 的内置集合在 Rust 标准库里都能找到对应。最大的区别有两个:Rust 的集合只装一种类型(Vec<i32> 里只能放 i32),HashMap 也不记得插入顺序。
| Python | Rust | 延伸学习 |
|---|---|---|
list |
Vec<T> |
Rust vector(Vec) |
tuple |
元组 (i32, String),或者一个带具名字段的小 struct |
Rust 元组 |
dict |
HashMap<K, V>(没有固定顺序!) |
Rust HashMap |
需要键有序的 dict |
BTreeMap<K, V> |
Rust HashMap |
需要保持插入顺序的 dict |
indexmap crate |
|
set、frozenset |
HashSet<T>、BTreeSet<T> |
HashSet 与 BTreeMap |
collections.Counter、defaultdict |
用 entry API 操作 HashMap:*map.entry(k).or_insert(0) += 1 |
Rust HashMap |
collections.deque |
VecDeque<T> |
VecDeque 与 BinaryHeap |
heapq(最小的先出) |
BinaryHeap<T>(最大的先出;包一层 Reverse 就变成最小的先出) |
VecDeque 与 BinaryHeap |
namedtuple |
struct |
Rust 结构体 |
bytes、bytearray |
&[u8]、Vec<u8> |
Rust slice(切片) |
切片:xs[1:3] |
slice:&xs[1..3](没有负数索引;用 xs.len() - 1) |
Rust slice(切片) |
工具链:从 pip 到 cargo
Section titled “工具链:从 pip 到 cargo”很多 Python 开发者看到这里会松一口气。Rust 只有一个官方工具 Cargo,它包办了 pip、venv、setuptools、pytest 等等的活。不存在“我们该用哪个打包工具?”这种争论。
| Python | Rust | 说明 |
|---|---|---|
python 解释器 |
rustc 编译器,通常通过 cargo 调用 |
先编译,再运行原生二进制文件。见 Rust 快速上手 |
| pyenv | rustup(再加一个 rust-toolchain.toml 文件来固定版本) |
见 Rust 工具与环境配置 |
| pip、poetry、uv | cargo |
构建、运行、测试、添加包、发布。见 Cargo 与 crate |
pip install requests |
cargo add reqwest |
只把 crate 加到当前项目的 Cargo.toml 里 |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
自动生成 |
| venv / virtualenv | 不需要 | 每个项目都有自己的依赖,构建产物放在自己的 target/ 目录里 |
| PyPI | crates.io | 每个已发布 crate 的 API 文档都在 docs.rs(英文)上 |
python main.py |
cargo run(要全速运行就用 cargo run --release) |
debug 构建运行起来慢得多;测速度要用 release 模式 |
pipx install ruff |
cargo install ripgrep |
安装一个命令行工具 |
| PyInstaller | cargo build --release |
得到一个原生二进制文件;用户不需要装 Rust |
| black | cargo fmt |
所有人共用一套标准风格 |
| ruff、pylint、flake8 | cargo clippy |
数百条实用的 lint,官方出品 |
| mypy、pyright | 编译器本身 | 类型检查不是可选的,也不是单独的一步 |
| pytest、unittest | 内置的 #[test] 和 cargo test(cargo nextest 是一个流行的、更快的测试运行器) |
见 Rust 测试 |
pytest 的 fixture 和 parametrize |
rstest |
|
| hypothesis | proptest |
基于属性的测试。见 测试进阶 |
unittest.mock |
mockall,或者手写一个实现了你的 trait 的假对象 |
|
| timeit、pytest-benchmark | criterion |
见 性能优化 |
| cProfile、py-spy | cargo flamegraph、perf,或任何原生性能分析器 |
|
| docstring + Sphinx | 用 Markdown 写的 /// 文档注释加 cargo doc |
文档里的代码示例会作为测试运行。见 Rust 文档 |
| Python REPL、Jupyter | Rust Playground;evcxr_repl 和 evcxr_jupyter |
Rust 没有内置的 REPL。平时做实验,通常就是建一个小小的 cargo run 项目 |
| VS Code + Pylance、PyCharm | VS Code + rust-analyzer,或者 JetBrains RustRover | rust-analyzer 提供补全、跳转到定义和行内报错 |
下面是一个小小的 Cargo.toml,你可以拿它和 pyproject.toml 比一比:
[package]name = "weather-cli"version = "0.1.0"edition = "2021"
[dependencies]reqwest = { version = "0.13", features = ["blocking", "json"] }serde = { version = "1", features = ["derive"] }clap = { version = "4", features = ["derive"] }库和框架:“那我们用什么来代替……?”
Section titled “库和框架:“那我们用什么来代替……?””“我用 requests、FastAPI 和 pandas,Rust 里对应的是什么?”下面是最接近的替代品。Python 的生态历史更长、规模更大,所以在某些领域(尤其是数据科学),Rust 的选择更少,也更年轻。
Web 与网络
Section titled “Web 与网络”| Python | Rust | 说明 |
|---|---|---|
| Flask、FastAPI | axum(我们在项目:用 Axum 写 Web API 里做了一个)、actix-web | 处理函数就是普通的 async fn;请求和响应类型来自你的结构体,有点像 FastAPI 配 pydantic |
| Django | 没有完整的对应物。Loco(loco-rs)是一个“开箱即用”的框架;大多数团队会把 axum、一个 ORM 和一个模板引擎组合起来用 |
没有内置的 admin 后台 |
| requests、httpx | reqwest(异步,也可以选用阻塞式客户端)、ureq(简单、阻塞式) |
见 网络编程 |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja(Jinja 作者本人写的)、tera、askama(模板在编译时检查) |
|
| BeautifulSoup | scraper |
数据与数据库
Section titled “数据与数据库”| Python | Rust | 说明 |
|---|---|---|
| SQLAlchemy ORM | Diesel(查询在编译时做类型检查)、SeaORM(异步、实体风格) | 数据库这一页用能跑的代码演示了这两个库 |
| psycopg、原生 SQL | sqlx:直接写 SQL,在编译时拿你的真实数据库来检查 | 异步,非常流行,不是 ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | 任务队列用 apalis;进程内的后台任务用 tokio::spawn |
数据科学与机器学习
Section titled “数据科学与机器学习”| Python | Rust | 说明 |
|---|---|---|
| pandas | Polars | 用 Rust 写的;你可能已经用过它的 Python API,那套 API 底下就是同一个 Rust 库 |
| NumPy | ndarray |
支持切片和广播的 N 维数组;周边功能比 NumPy 少得多 |
| scikit-learn | linfa、smartcore |
算法更少,社区也小得多 |
| PyTorch | candle(crate 名是 candle-core)、burn |
适合运行模型;大部分训练工作仍然在 Python 里做 |
| matplotlib | plotters |
|
csv 模块 |
csv crate + serde |
日常基础组件
Section titled “日常基础组件”| Python | Rust | 说明 |
|---|---|---|
json |
serde + serde_json |
事实标准;还支持 TOML、YAML 等格式。见 用 Serde 处理 JSON |
| pydantic | 用 serde 做解析,再用 validator 或 garde 写校验规则 |
你的结构体就是 schema |
| argparse、click、typer | clap |
把命令行参数定义成一个结构体。见 项目:用 clap 写命令行工具 |
logging |
tracing,或 log + env_logger |
见 日志 |
re |
regex |
不支持环视和反向引用,但保证线性时间;fancy-regex 支持它们。见 正则表达式 |
datetime |
chrono 或 jiff |
见 日期与时间 |
random |
rand |
见 随机数 |
asyncio |
tokio |
见 async/await |
multiprocessing.Pool.map |
rayon:把 .iter() 改成 .par_iter() |
|
itertools |
大部分已经内置在 Iterator 里;其余的由 itertools crate 补上 |
|
os、pathlib、subprocess |
std::env、std::path、std::fs、std::process |
见 文件读写、环境变量与进程 |
对照看:类变成 struct + impl
Section titled “对照看:类变成 struct + impl”下面是一个小小的 Python 类:
class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount
def __str__(self): return f"{self.owner} 的余额是 {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)再看 Rust 版:
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` 只是一种约定,不是关键字。Rust 没有默认参数, // 所以调用者总是要把两个值都传进来。 fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self`:这个方法会修改账户。 fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// 这就是 Rust 版的 __str__。impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} 的余额是 {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}输出:
Ada 的余额是 150有几处不一样。字段连同类型都要事先声明好,所以你没法像 Python 里写 account.nickname = "A" 那样,事后再加一个新属性。数据(struct)和行为(impl)分别放在两个块里。每个方法都要说明自己是只读(&self)还是会修改(&mut self)这个值;而且因为 deposit 会修改账户,变量必须声明成 let mut。Rust 没有默认参数;如果你想要一个余额从零开始的账户,就再加一个函数,比如 fn empty(owner: &str) -> Self。
对照看:鸭子类型变成 trait
Section titled “对照看:鸭子类型变成 trait”Python 不在乎一个对象是什么类,只在乎它有没有对应的方法:
class Duck: def speak(self): return "嘎嘎"
class Robot: def speak(self): return "哔哔啵啵"
def introduce(thing): print(f"它说:{thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError,但要等这一行运行时才会出现在 Rust 里,“有一个 speak 方法”这件事要明明白白地写成一个 trait,每个类型再用 impl 块主动实现它:
trait Speak { fn speak(&self) -> String;}
struct Duck;struct Robot;
impl Speak for Duck { fn speak(&self) -> String { "嘎嘎".to_string() }}
impl Speak for Robot { fn speak(&self) -> String { "哔哔啵啵".to_string() }}
// 接受任何实现了 Speak 的类型。fn introduce(thing: &dyn Speak) { println!("它说:{}", thing.speak());}
fn main() { let things: Vec<Box<dyn Speak>> = vec![Box::new(Duck), Box::new(Robot)]; for thing in &things { introduce(thing.as_ref()); }
// introduce(&42); // 编译错误:`{integer}` 没有实现 Speak}输出:
它说:嘎嘎它说:哔哔啵啵骨子里它还是鸭子类型,只不过由编译器来检查:introduce(&42) 在程序运行之前就会报错,而不是在生产环境里抛出 AttributeError。如果你用过 typing.Protocol,会觉得 trait 很自然。装着不同类型的列表需要用 Box<dyn Speak>,因为 Rust 必须知道怎么在每个元素上调用这个方法(见 trait 对象(dyn))。另外,你还可以为别人写的类型实现自己的 trait,比如 String 或 i32。
对照看:None 变成 Option
Section titled “对照看:None 变成 Option”在 Python 里,任何值都可能悄悄是 None,要等你用它的时候才发现:
def find_email(user_id): return emails.get(user_id) # 找不到时返回 None
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'在 Rust 里,一个可能缺失的值的类型是 Option<T>。在处理好 None 的情况之前,你没法把它当普通值来用:
fn find_email(user_id: u32) -> Option<String> { match user_id { _ => None, }}
fn main() { for id in [1, 3] { match find_email(id) { Some(email) => println!("{id}:{}", email.to_uppercase()), None => println!("{id}:没有登记邮箱"), } }
// 相当于 Python 的 `find_email(3) or "未知"`。 let email = find_email(3).unwrap_or("未知".to_string()); println!("{email}");
// 如果里面有值,就把它变换一下。 let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}输出:
3:没有登记邮箱未知Some("[email protected]")Rust 编译器不让你在 Option<String> 上调用 .to_uppercase(),所以“'NoneType' object has no attribute”这种错误根本不可能发生。Rust 里的 String 永远是一个真正的字符串。辅助方法可以在 Rust Option 这一页学,简短写法 if let Some(email) = ... 在 if let / while let 这一页。
对照看:异常变成 Result 和 ?
Section titled “对照看:异常变成 Result 和 ?”在 Python 里,int() 会抛出异常,你要在调用栈上层的某个地方把它捕获:
def parse_port(text): port = int(text) # 可能抛出 ValueError if not 1 <= port <= 65535: raise ValueError(f"端口 {port} 超出范围") return port
for text in ["8080", "http", "70000"]: try: print(f"端口 {parse_port(text)}") except ValueError as e: print(f"错误:{e}")在 Rust 里,可能失败的函数会在返回类型里写明。? 运算符的意思是“如果这一步失败了,就立刻把错误返回给我的调用者”。它起的作用就是让异常向上冒泡,只不过你在代码里看得见:
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} 不是数字({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("端口 {port} 超出范围")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("端口 {port}"), Err(e) => println!("错误:{e}"), } }}输出:
端口 8080错误:"http" 不是数字(invalid digit found in string)错误:端口 70000 超出范围invalid digit found in string 是标准库自带的英文错误信息,意思是“字符串里有无效的数字”。
最大的区别在于:在 Python 里,任何函数都可能抛异常,签名上却什么也看不出来。在 Rust 里,Result 是签名的一部分,调用者不可能忘记处理它。学习阶段用 String 当错误类型完全没问题;真实项目会定义自己的错误枚举(自定义错误类型),或者用 anyhow crate,它用起来很像“想抛什么异常就抛什么”(anyhow 与 thiserror)。
对照看:推导式和 dict 变成迭代器和 HashMap
Section titled “对照看:推导式和 dict 变成迭代器和 HashMap”列表推导式和用 dict 计数,是 Python 里天天都在写的东西:
text = "the cat sat on the mat the end"
loud = [w.upper() for w in text.split() if len(w) == 3]print(loud)
counts = {}for word in text.split(): counts[word] = counts.get(word, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]for word, n in top: print(f"{word}: {n}")在 Rust 里,推导式变成一条以 collect() 结尾的迭代器链,dict 变成 HashMap:
use std::collections::HashMap;
fn main() { let text = "the cat sat on the mat the end";
// [w.upper() for w in text.split() if len(w) == 3] let loud: Vec<String> = text .split_whitespace() .filter(|w| w.len() == 3) .map(|w| w.to_uppercase()) .collect(); println!("{loud:?}");
// counts[word] = counts.get(word, 0) + 1 let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; }
// 出现次数多的排前面;次数相同的按字母顺序排。 let mut top: Vec<(&str, u32)> = counts.into_iter().collect(); top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0))); for (word, n) in top.iter().take(3) { println!("{word}: {n}"); }}输出:
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1这里的句子特意保留了英文:程序要按单词的字节长度(w.len() == 3)筛选、转成大写,还要按字母排序,换成中文就看不出效果了。
迭代器链是从左往右读的,而不是从里往外读,但思路是一样的。和生成器表达式一样,它是惰性的:在 collect()、sum()、count() 或者 for 循环把值拉出来之前,什么都不会发生。collect() 需要知道要构建出什么,所以 loud 的类型写成了 Vec<String>。还要注意排序这一步:Python 的 dict 记得插入顺序,Rust 的 HashMap 却不记得,所以打印之前我们总是先排序。排序的内容在排序与查找这一页。
对照看:GIL 与真正的线程
Section titled “对照看:GIL 与真正的线程”在标准的 CPython 里,全局解释器锁(GIL)让同一时间只有一个线程能执行 Python 字节码。等待网络时多线程有帮助,但像下面这种 CPU 密集型的工作,线程再多也不会变快,所以 Python 开发者会改用 multiprocessing:
from concurrent.futures import ThreadPoolExecutor
def is_prime(n): if n < 2: return False d = 2 while d * d <= n: if n % d == 0: return False d += 1 return True
def count_primes(lo, hi): return sum(1 for n in range(lo, hi) if is_prime(n))
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]with ThreadPoolExecutor(4) as pool: counts = list(pool.map(lambda r: count_primes(*r), ranges))print("总计:", sum(counts))Rust 没有 GIL。每个 thread::spawn 都是一个真正的操作系统线程,它们会同时在不同的 CPU 核心上运行:
use std::thread;
fn is_prime(n: u32) -> bool { if n < 2 { return false; } let mut d = 2; while d * d <= n { if n % d == 0 { return false; } d += 1; } true}
fn count_primes(lo: u32, hi: u32) -> usize { (lo..hi).filter(|&n| is_prime(n)).count()}
fn main() { let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
// 每个区间都有自己的操作系统线程,它们真的在同时运行。 let counts: Vec<usize> = thread::scope(|s| { let handles: Vec<_> = ranges .iter() .map(|&(lo, hi)| s.spawn(move || count_primes(lo, hi))) .collect(); handles.into_iter().map(|h| h.join().unwrap()).collect() });
for ((lo, hi), count) in ranges.iter().zip(&counts) { println!("{lo}..{hi}:{count} 个素数"); } println!("总计:{}", counts.iter().sum::<usize>());}输出:
0..25000:2762 个素数25000..50000:2371 个素数50000..75000:2260 个素数75000..100000:2199 个素数总计:9592thread::scope 会等所有线程结束后才返回,所以这些线程可以放心地借用局部数据。没有了 GIL,你可能会担心两个线程同时修改同一个值。Rust 在编译时就处理了这个问题:会被修改的共享数据必须包在 Arc<Mutex<T>> 这样的东西里,否则程序编译不过(见 共享状态)。在真实项目里,rayon crate 能让这段代码更短,有点像 multiprocessing.Pool.map,只不过用的是线程:
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Python 这边也在变化:Python 3.13 加入了一个可选的、没有 GIL 的“自由线程”(free-threaded)构建。不过大多数人安装和部署的标准 CPython 仍然有 GIL,很多 C 扩展也还没准备好支持自由线程。
用 Rust 写 Python 扩展(PyO3 + maturin)
Section titled “用 Rust 写 Python 扩展(PyO3 + maturin)”用 Rust 不代表要离开 Python。PyO3(英文)让你用 Rust 写 Python 模块,maturin(英文)再把它构建成一个普通的 wheel,可以用 pip 安装。Polars、pydantic-core 和很多其他高性能 Python 库就是这么做出来的。
建一个项目(在已激活的虚拟环境里):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin 会帮你写好 Cargo.toml 和 pyproject.toml。然后把 Rust 函数放进 src/lib.rs:
use pyo3::prelude::*;
/// 用 Rust 写的 Python 模块。#[pymodule]mod fastmath { use pyo3::prelude::*;
/// 把一组数字的平方加起来。 #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// 统计一段文本里的单词数。 #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}用 maturin develop 把它构建进你的虚拟环境(在意速度时加上 --release),然后就能像其他模块一样在 Python 里使用:
import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integerPyO3 会帮你转换类型:Python 里由整数组成的 list 会变成 Vec<i64>,str 会变成 &str,文档注释会变成 Python 的 docstring。类型传错了,就会变成一个普通的 Python TypeError。准备好分享时,maturin build --release 会打出一个 wheel,maturin publish 会把它上传到 PyPI。一条好用的经验:先测量,再只把最热的那个循环搬到 Rust。在 Python 和 Rust 之间来回调用有一点开销,所以一次干很多活的调用,胜过一百万次零碎的小调用。
Python 开发者容易踩的坑
Section titled “Python 开发者容易踩的坑”从 Python 转过来的人都会遇到同样的几个坎。提前知道会轻松不少。
赋值会移动值
Section titled “赋值会移动值”在 Python 里,b = a 会让同一个列表有两个名字。在 Rust 里,let b = a; 会把 Vec 或 String 的所有权移动给 b,之后 a 就不能再用了:
fn main() { let a = vec![1, 2, 3]; let mut b = a; b.push(4); println!("{a:?}");}error[E0382]: borrow of moved value: `a` --> src/main.rs:5:16 |2 | let a = vec![1, 2, 3]; | - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let mut b = a; | - value moved here4 | b.push(4);5 | println!("{a:?}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let mut b = a.clone(); | ++++++++编译器说的是:a 的值已经移动到了 b,移动之后又被借用了;它还建议,如果能接受性能开销,可以克隆这个值。解决办法是借用(只需要读的话,写 let b = &a;),或者有意识地做一份真正的副本(a.clone())。这一条规则,也就是所有权,是你要学的最重要的新概念。请慢慢读那一页和 Rust 借用。
其他常见的坑
Section titled “其他常见的坑”- 整数有大小。 Python 的
int永远不会溢出。Rust 的i32最大到 2,147,483,647,在 debug 构建里超过它就会 panic。大数字就用i64或u64,另外请看 Rust 整数溢出。 - 除法和取余的规则不一样。 在 Rust 里
7 / 2是3,因为两边都是整数(相当于 Python 的//)。想得到3.5,就写7.0 / 2.0。另外,-7 % 3在 Rust 里是-1,在 Python 里却是2;想得到 Python 的结果,就用(-7_i32).rem_euclid(3)。 - 不能混用数字类型。
let x: f64 = 2.5 * 3;编译不过,因为3是整数。写成3.0,或者用as f64转换(见 Rust 类型转换)。 - 不能用下标访问字符串。
word[0]是编译错误,因为 Rust 的字符串是 UTF-8 编码的,一个“字符”可能占好几个字节(一个汉字就占 3 个字节)。用word.chars().next()或word.chars().nth(i)。见 Rust 字符串。 String与&str。 Python 只有一种str类型。Rust 有两种:String拥有它的文本,&str借用文本。函数参数通常用&str,结构体字段通常存String。- 变量默认不可变。 值需要修改时就写
let mut。你忘了的话,编译器会提醒你。 - 大括号和分号。 缩进对 Rust 来说无所谓,
{ }才算数。另外,函数的最后一行如果不带分号,就是它的返回值:fn double(x: i32) -> i32 { x * 2 }。 - 每个
if和match都必须完整、类型正确。 没有真值判断:if count编译不过,要写if count > 0。match必须覆盖所有情况。 - HashMap 没有顺序。 Python 的
dict记得插入顺序,Rust 的HashMap不记得。打印前先排序,或者用BTreeMap,或者加上indexmapcrate。 - 有一个编译步骤。 没有 REPL,一个依赖很多 crate 的项目从零开始构建要花点时间。改代码的时候用
cargo check(它跳过代码生成,快得多),测速度的时候用cargo run --release。 - 借用检查器会跟你较劲。 在 Python 里很常见的写法,比如很多对象同时持有并修改同一个共享对象,或者对象指回它的父对象,在 Rust 里需要用智能指针或者换一种设计。做好心理准备,头几周会有点受挫。刚开始觉得别扭很正常,之后会轻松很多。
Python 开发者的学习路线
Section titled “Python 开发者的学习路线”你不必按顺序读完整个网站。作为 Python 开发者,下面这条路线会把时间花在 Rust 和你熟悉的东西差别最大的地方:
- Rust 快速上手和 Rust 工具与环境配置:安装 Rust,运行
cargo new。 - Rust 变量、Rust 数据类型和 Rust 整数溢出:类型是你要养成的第一个新习惯。
- Rust 函数、Rust if…else 和 Rust loop 循环:很快就能读完;留意表达式和那些“消失”的分号。
- Rust 所有权,然后是 Rust 借用,再然后是 Rust slice(切片):对你来说最重要的几页,慢慢来。
- Rust 字符串:
String与&str,以及为什么不能用下标访问字符串。 - Rust 结构体和 Rust 枚举,然后是 Rust match 和 Rust 模式匹配。
- Rust Option 和 Rust Result,然后是自定义错误类型。
- Rust vector(Vec)、Rust HashMap 和 HashSet 与 BTreeMap。
- Rust 闭包和 Rust 迭代器:你的推导式和生成器。
- Rust trait、Rust 泛型、常用 trait 和 trait 对象(dyn)。
- Rust 模块、Cargo 与 crate 和 Rust 测试。
- Rust 生命周期和 Rust 智能指针。
- Rust 并发、共享状态和 async/await。
- 用 Serde 处理 JSON,然后动手做一个 clap 命令行工具或者一个 Web API。
两道题都会给你一段 Python 代码,请把它移植到 Rust。
1. 移植一个推导式和一个 dict
Section titled “1. 移植一个推导式和一个 dict”移植下面这段 Python 代码。提示:要按顺序打印名字,就把它们收集到一个 Vec<&str> 里,调用 .sort(),再 .join(", ")。算平均分时,先用 as f64 转换再相除。
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("及格:" + ", ".join(passed))
odd_squares = [n * n for n in range(1, 10) if n % 2 == 1]print(odd_squares)
average = sum(scores.values()) / len(scores)print(f"平均分:{average:.1f}")查看答案
use std::collections::HashMap;
fn main() { let scores: HashMap<&str, u32> = HashMap::from([("Ada", 92), ("Bob", 47), ("Cy", 75), ("Dee", 38)]);
let mut passed: Vec<&str> = scores .iter() .filter(|(_, score)| **score >= 50) .map(|(name, _)| *name) .collect(); passed.sort(); println!("及格:{}", passed.join(", "));
let odd_squares: Vec<u32> = (1..10).filter(|n| n % 2 == 1).map(|n| n * n).collect(); println!("{odd_squares:?}");
let total: u32 = scores.values().sum(); let average = total as f64 / scores.len() as f64; println!("平均分:{average:.1}");}输出:
及格:Ada, Cy[1, 9, 25, 49, 81]平均分:63.0遍历 HashMap 得到的是一对对引用,所以你会看到 **score 和 *name:它们顺着引用找回真正的值。这里的 sort() 不能省:没有它,名字可能以任意顺序出现。total as f64 也是必须的,因为 Rust 从不在背后偷偷把整数变成浮点数。
2. 把 try/except 移植成 Result
Section titled “2. 把 try/except 移植成 Result”移植下面这段 Python 代码。每一行的格式应该是 name: age。提示:line.split_once(':') 返回一个 Option<(&str, &str)>;用 .ok_or(...) 把它变成 Result,然后用 ?。让你的函数返回 Result<(String, u32), String>。
def parse_line(line): if ":" not in line: raise ValueError("缺少 ':'") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"年龄无效:{e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: 四十"]: try: name, age = parse_line(line) ages[name] = age print(f"成功:{name},{age} 岁") except ValueError as e: print(f"跳过 {line!r}:{e}")print(f"已保存 {len(ages)} 条")查看答案
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("缺少 ':'")?; let age: u32 = age.trim().parse().map_err(|e| format!("年龄无效:{e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: 四十"] { match parse_line(line) { Ok((name, age)) => { println!("成功:{name},{age} 岁"); ages.insert(name, age); } Err(e) => println!("跳过 {line:?}:{e}"), } }
println!("已保存 {} 条", ages.len());}输出:
成功:Ada,36 岁跳过 "Grace 45":缺少 ':'跳过 "Linus: 四十":年龄无效:invalid digit found in string已保存 1 条两个 raise 变成了 Err 值,try/except 变成了一个 match。ok_or("缺少 ':'") 后面的 ? 照样能用,虽然这个错误是 &str,而函数返回的错误是 String,因为 ? 会用 From 转换错误类型。注意 Ok 分支里的顺序:先打印,再用 insert 把 name 移动进 map。把这两行对调一下,编译器就会告诉你 name 已经被移动了。
- Python 的很多东西都能直接带过来:遍历迭代器的循环、元组、闭包、
match,以及推导式风格的数据处理(变成迭代器链)。 - 最大的转变是:类型在运行前就检查好,用所有权代替垃圾回收器,用
Option代替None,用Result加?代替异常。 - 类变成
struct+impl,鸭子类型变成 trait,with块变成自动运行的Drop。HashMap没有顺序,整数有固定的大小。 - Rust 没有 GIL:线程真正并行运行,编译器会阻止数据竞争。
- Cargo 取代了 pip、venv 这一大家子;
cargo fmt、cargo clippy和cargo test取代了 black、ruff 和 pytest。 - 两种语言可以混着用:PyO3 和 maturin 能把 Rust 代码变成一个 Python 模块。
小测验
1. Rust 用什么来代替 Python 的 None?
2. 在 Python 里,b = a 会让同一个列表有两个名字。在 Rust 里,对一个 Vec 写 let b = a; 会怎样?
3. 在 Rust 里,两个数都是整数时,7 / 2 的结果是什么?
4. 你想用 Rust 给 Python 项目里一个很慢的函数提速。该用哪些工具?
得分: 0 / 4
- 《Rust 程序设计语言》简体中文版:官方教程。对 Python 开发者来说,第 4 章(所有权)、第 6 章(枚举和
Option)、第 9 章(错误处理)和第 13 章(闭包和迭代器)最重要。 - PyO3 用户指南(英文):关于从 Python 调用 Rust(以及从 Rust 调用 Python)的一切。
- Maturin 用户指南(英文):构建、测试和发布由 Rust 驱动的 Python 包。
- 通过例子学 Rust(Rust by Example 中文版):一个个可以运行的小例子,适合喜欢通过读代码来学习的人。
- Are We Learning Yet?(英文):Rust 机器学习和数据处理 crate 的概览。