Python 开发者学 Rust,听起来像是换赛道,但实际效果更像给 Python 代码装了一副透视镜——Rust 编译器逼你面对的问题,恰恰是 Python 里容易被动态特性掩盖的隐患。编译器不通过的代码,在 Python 里可能跑得好好的,直到生产环境给你一记闷棍。
下面从几个具体维度看,Rust 的"严苛"如何反过来塑造更健壮的 Python 习惯。
类型不是文档,是契约
Python 的类型注解从 3.5 起就有了,但多数项目停留在"写了比不写好"的层面,mypy 一跑满屏红字干脆关掉。Rust 不给你这个选项——编译器直接拒绝类型不匹配的代码。
这种强制体验会改变你对 Python 类型的态度:不再把 Optional[str] 当装饰,而是认真考虑 None 到底是不是合法输入。
对比一下:
# 习惯性写法:类型注解是摆设
def get_user_name(user_id):
# 返回 str?还是 None?调用者猜
result = db.query("SELECT name FROM users WHERE id = ?", user_id)
return result # 可能是 None,但签名没说
# Rust 思维改造后:类型是契约,None 必须显式声明
from typing import Optional
def get_user_name(user_id: int) -> Optional[str]:
"""返回用户名,用户不存在时返回 None。调用方必须处理 None。"""
result = db.query("SELECT name FROM users WHERE id = ?", user_id)
return result
Rust 的 Option<T> 让你无法忽略 None——必须 .unwrap() 或显式匹配。回到 Python,你会自然地在调用处加判断:
name = get_user_name(42)
if name is None:
# Rust 的 match None 分支在 Python 里变成显式处理
logger.warning("User 42 not found")
return "anonymous"
print(f"Hello, {name}")
错误处理:从 try/except 大网到精确捕获
Python 的 try/except Exception 太方便了,一网打尽所有异常,连你没想到的 KeyError 也被吞掉。Rust 的 Result<T, E> 模式要求你逐个处理可能的错误类型,编译器检查你是否遗漏。
这种精确性带回 Python 后,你的异常处理会从"兜底"变成"逐个击破":
# 兜底式:出了问题只知道"出问题了"
try:
data = fetch_api_data(url)
parsed = parse_json(data)
save_to_db(parsed)
except Exception as e:
logger.error(f"Something went wrong: {e}")
raise
# Rust 思维改造后:每个失败点有名字,处理路径清晰
class FetchError(Exception): pass
class ParseError(Exception): pass
class DBError(Exception): pass
def process_data(url: str) -> None:
try:
data = fetch_api_data(url)
except FetchError as e:
logger.error(f"API 请求失败: {e}")
raise
try:
parsed = parse_json(data)
except ParseError as e:
logger.error(f"JSON 解析失败,原始数据: {data[:200]}")
raise
try:
save_to_db(parsed)
except DBError as e:
logger.error(f"数据库写入失败: {e}")
# 可以选择重试而非直接 raise
retry_save(parsed, max_attempts=3)
自定义异常类看起来啰嗦,但它把 Rust 的 enum ErrorKind 思路搬了过来:调用方可以按类型选择恢复策略,而不是在 Exception 的汪洋里捞针。
所有权意识:让资源泄漏变得显眼
Rust 的所有权系统让每个值只有一个主人,编译器追踪谁负责释放。Python 有 GC,内存不用手动管,但文件句柄、数据库连接、锁这些资源照样会泄漏——只是 Python 不会在编译时告诉你。
写 Rust 之后,你会更在意资源的获取和释放边界:
# 以前:靠 GC 和运气
def process_files(paths):
results = []
for p in paths:
f = open(p) # 谁关?GC 某个时刻?还是永远不关?
results.append(f.read())
return results
# Rust 思维改造后:资源生命周期必须显式
from contextlib import closing
import sqlite3
def process_files(paths: list[str]) -> list[str]:
results = []
for p in paths:
with open(p) as f: # 作用域结束 = Rust 的 drop,显式释放
results.append(f.read())
return results
def query_user_data(db_path: str, user_id: int) -> dict:
# 连接的生命周期绑定到这个函数,不泄漏到外部
with closing(sqlite3.connect(db_path)) as conn:
with closing(conn.cursor()) as cur:
cur.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = cur.fetchone()
return dict(row) if row else {}
with 和 closing 就是 Python 版的 Rust 所有权——进入作用域获取资源,离开作用域释放资源。区别是 Python 不强制你用,但 Rust 程序员会主动用,因为已经习惯了编译器的催促。
不可变默认:减少意外修改
Rust 变量默认不可变,要改必须加 mut。Python 没有这个机制,但你可以用类型注解和编码习惯模拟:
from typing import Final
# Rust 思维:标记不该变的东西
MAX_CONNECTIONS: Final[int] = 100
API_BASE_URL: Final[str] = "https://api.example.com"
# 函数参数用文档约定不可变语义
def calculate_total(items: list[dict]) -> float:
"""计算总价。不修改 items 内容——调用方可以放心传原始列表。"""
# Rust 习惯:不就地修改,返回新值
return sum(item["price"] * item["quantity"] for item in items)
# 对比就地修改版本(Rust 需要 &mut 才允许,Python 默认允许)
def calculate_total_mut(items: list[dict]) -> float:
"""计算总价并给每个 item 加上 _total 字段。调用方注意:items 被修改了。"""
total = 0.0
for item in items:
item["_total"] = item["price"] * item["quantity"] # 就地修改
total += item["_total"]
return total
Final 是运行时不起作用的注解,但 IDE 和类型检查器会警告你违反约定。更重要的是思维转变:默认不改,需要改时显式声明——这和 Rust 的 let mut 一脉相承。
实战练习:用 PyO3 写一个小模块
光看思路不够,动手写一段 Rust 编译给 Python 调用的代码,能同时体验两边的约束。以下是一个最小可运行的 PyO3 项目:
# 1. 创建项目
mkdir rust_for_py && cd rust_for_py
pip install maturin
# 2. 初始化 Rust 项目
maturin init --lib
# 选择 pyo3 绑定
# 3. 编辑 src/lib.rs
use pyo3::prelude::*;
/// 计算字符串中每个字符的出现次数
/// Rust 版本:类型明确,返回值不可变,错误由编译器拦截
#[pyfunction]
fn char_counts(text: &str) -> PyResult<Vec<(char, usize)>> {
// Rust 强制你声明类型、处理可能的错误
let mut counts: std::collections::HashMap<char, usize> =
std::collections::HashMap::new();
for ch in text.chars() {
*counts.entry(ch).or_insert(0) += 1;
}
// 转成排序后的 Vec 返回给 Python
let mut result: Vec<(char, usize)> = counts.into_iter().collect();
result.sort_by(|a, b| b.1.cmp(&a.1));
Ok(result)
}
/// 一个 Python 模块
#[pymodule]
fn rust_for_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(char_counts, m)?)?;
Ok(())
}
# 4. 编译并开发模式安装
maturin develop
# 5. 在 Python 中使用
python -c "
from rust_for_py import char_counts
result = char_counts('hello rust, hello python')
print(result)
# [(' ', 2), ('h', 2), ('l', 4), ('e', 2), ('o', 3), ...]
"
写这段 Rust 代码时,你会经历:类型必须声明、引用必须标注生命周期、PyResult 必须处理——这些约束在 Python 里全是可选的。体验过"不通过就不编译"之后,回到 Python 你会更自觉地加上类型注解和错误处理,哪怕编译器不强制。
采纳建议与边界
学 Rust 改善 Python 不是万能药,有几点需要注意:
- 不要把 Python 写成 Rust。Python 的动态特性、列表推导、装饰器是真正的生产力工具,Rust 思维是补充而非替代。该用
dict.get(key, default)的地方别写成match模式。 - 优先在关键路径加约束。入口函数、公共 API、数据转换层——这些地方加类型注解和精确异常最有价值。内部工具函数可以保持灵活。
- PyO3 适合性能瓶颈。纯 Python 够用的场景不必引入 Rust 编译链,只在计算密集或延迟敏感处用 Rust 扩展。
- 学习曲线真实存在。Rust 的所有权和生命周期需要几十小时才能上手,回报是长期的编码习惯改善,不是立竿见影的效率提升。
一个务实的路径:先在 Python 项目里严格执行类型注解和精确异常处理,感受"自驱约束"的效果;再挑一个计算密集的小模块用 PyO3 重写,体验编译器驱动的约束。两轮下来,你对"什么该约束、什么该灵活"的判断力会明显不同。