跳转到内容

面向 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 对比。

把这张表当成一本“短语手册”来用。最后一列链接到本站讲解 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(切片)

很多 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 的选择更少,也更年轻。

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
Python Rust 说明
SQLAlchemy ORM Diesel(查询在编译时做类型检查)、SeaORM(异步、实体风格) 数据库这一页用能跑的代码演示了这两个库
psycopg、原生 SQL sqlx:直接写 SQL,在编译时拿你的真实数据库来检查 异步,非常流行,不是 ORM
sqlite3 rusqlite
redis-py redis
Celery 任务队列用 apalis;进程内的后台任务用 tokio::spawn
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
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 见 文件读写、环境变量与进程

下面是一个小小的 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。

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。

在 Python 里,任何值都可能悄悄是 None,要等你用它的时候才发现:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
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 {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => 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:没有登记邮箱
未知
▶ 动手试一试

Rust 编译器不让你在 Option<String> 上调用 .to_uppercase(),所以“'NoneType' object has no attribute”这种错误根本不可能发生。Rust 里的 String 永远是一个真正的字符串。辅助方法可以在 Rust Option 这一页学,简短写法 if let Some(email) = ... 在 if let / while let 这一页。

在 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: 3
cat: 1
end: 1
▶ 动手试一试

这里的句子特意保留了英文:程序要按单词的字节长度(w.len() == 3)筛选、转成大写,还要按字母排序,换成中文就看不出效果了。

迭代器链是从左往右读的,而不是从里往外读,但思路是一样的。和生成器表达式一样,它是惰性的:在 collect()、sum()、count() 或者 for 循环把值拉出来之前,什么都不会发生。collect() 需要知道要构建出什么,所以 loud 的类型写成了 Vec<String>。还要注意排序这一步:Python 的 dict 记得插入顺序,Rust 的 HashMap 却不记得,所以打印之前我们总是先排序。排序的内容在排序与查找这一页。

在标准的 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 个素数
总计:9592
▶ 动手试一试

thread::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 库就是这么做出来的。

建一个项目(在已激活的虚拟环境里):

Terminal window
pip install maturin
maturin new --bindings pyo3 fastmath
cd fastmath

maturin 会帮你写好 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])) # 30
print(fastmath.word_count("the quick brown fox")) # 4
fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integer

PyO3 会帮你转换类型:Python 里由整数组成的 list 会变成 Vec<i64>,str 会变成 &str,文档注释会变成 Python 的 docstring。类型传错了,就会变成一个普通的 Python TypeError。准备好分享时,maturin build --release 会打出一个 wheel,maturin publish 会把它上传到 PyPI。一条好用的经验:先测量,再只把最热的那个循环搬到 Rust。在 Python 和 Rust 之间来回调用有一点开销,所以一次干很多活的调用,胜过一百万次零碎的小调用。

从 Python 转过来的人都会遇到同样的几个坎。提前知道会轻松不少。

在 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` trait
3 | let mut b = a;
| - value moved here
4 | 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 借用。

  • 整数有大小。 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,或者加上 indexmap crate。
  • 有一个编译步骤。 没有 REPL,一个依赖很多 crate 的项目从零开始构建要花点时间。改代码的时候用 cargo check(它跳过代码生成,快得多),测速度的时候用 cargo run --release。
  • 借用检查器会跟你较劲。 在 Python 里很常见的写法,比如很多对象同时持有并修改同一个共享对象,或者对象指回它的父对象,在 Rust 里需要用智能指针或者换一种设计。做好心理准备,头几周会有点受挫。刚开始觉得别扭很正常,之后会轻松很多。

你不必按顺序读完整个网站。作为 Python 开发者,下面这条路线会把时间花在 Rust 和你熟悉的东西差别最大的地方:

  1. Rust 快速上手和 Rust 工具与环境配置:安装 Rust,运行 cargo new。
  2. Rust 变量、Rust 数据类型和 Rust 整数溢出:类型是你要养成的第一个新习惯。
  3. Rust 函数、Rust if…else 和 Rust loop 循环:很快就能读完;留意表达式和那些“消失”的分号。
  4. Rust 所有权,然后是 Rust 借用,再然后是 Rust slice(切片):对你来说最重要的几页,慢慢来。
  5. Rust 字符串:String 与 &str,以及为什么不能用下标访问字符串。
  6. Rust 结构体和 Rust 枚举,然后是 Rust match 和 Rust 模式匹配。
  7. Rust Option 和 Rust Result,然后是自定义错误类型。
  8. Rust vector(Vec)、Rust HashMap 和 HashSet 与 BTreeMap。
  9. Rust 闭包和 Rust 迭代器:你的推导式和生成器。
  10. Rust trait、Rust 泛型、常用 trait 和 trait 对象(dyn)。
  11. Rust 模块、Cargo 与 crate 和 Rust 测试。
  12. Rust 生命周期和 Rust 智能指针。
  13. Rust 并发、共享状态和 async/await。
  14. 用 Serde 处理 JSON,然后动手做一个 clap 命令行工具或者一个 Web API。

两道题都会给你一段 Python 代码,请把它移植到 Rust。

移植下面这段 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 从不在背后偷偷把整数变成浮点数。

移植下面这段 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