学 Rust 怎么让你写出更好的 Python

2026-04-24 16 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

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 {}

withclosing 就是 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 重写,体验编译器驱动的约束。两轮下来,你对"什么该约束、什么该灵活"的判断力会明显不同。


相关推荐