Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Частые паттерны Python в Rust

Что вы узнаете: как переводить dict → struct, class → struct+impl, списочные включения → цепочки итераторов, декоратор → функции высшего порядка и контекстный менеджер → Drop/RAII. Плюс основные крейты и стратегия постепенного внедрения.

Сложность: 🟡 Средний

Словарь → структура

# Python — dict как контейнер данных (очень распространённо)
user = {
    "name": "Alice",
    "age": 30,
    "email": "alice@example.com",
    "active": True,
}
print(user["name"])
#![allow(unused)]
fn main() {
// Rust — структура с именованными полями
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct User {
    name: String,
    age: i32,
    email: String,
    active: bool,
}

let user = User {
    name: "Alice".into(),
    age: 30,
    email: "alice@example.com".into(),
    active: true,
};
println!("{}", user.name);
}

Контекстный менеджер → RAII (Drop)

# Python — контекстный менеджер для освобождения ресурсов
class FileManager:
    def __init__(self, path):
        self.file = open(path, 'w')

    def __enter__(self):
        return self.file

    def __exit__(self, *args):
        self.file.close()

with FileManager("output.txt") as f:
    f.write("hello")
# Файл автоматически закрывается при выходе из блока `with`
#![allow(unused)]
fn main() {
// Rust — RAII: трейт Drop выполняется, когда значение выходит из области видимости
use std::fs::File;
use std::io::Write;

fn write_file() -> std::io::Result<()> {
    let mut file = File::create("output.txt")?;
    file.write_all(b"hello")?;
    Ok(())
    // Файл автоматически закрывается, когда `file` выходит из области видимости
    // `with` не нужен: RAII делает всё сам!
}
}

Декоратор → функция высшего порядка или макрос

# Python — декоратор для замера времени
import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} выполнилась за {elapsed:.4f}s")
        return result
    return wrapper

@timed
def slow_function():
    time.sleep(1)
#![allow(unused)]
fn main() {
// Rust — никаких декораторов: используйте функции-обёртки или макросы
use std::time::Instant;

fn timed<F, R>(name: &str, f: F) -> R
where
    F: FnOnce() -> R,
{
    let start = Instant::now();
    let result = f();
    println!("{} выполнилась за {:.4?}", name, start.elapsed());
    result
}

// Использование:
let result = timed("slow_function", || {
    std::thread::sleep(std::time::Duration::from_secs(1));
    42
});
}

Конвейер итераторов (обработка данных)

# Python — цепочка преобразований
import csv
from collections import Counter

def analyze_sales(filename):
    with open(filename) as f:
        reader = csv.DictReader(f)
        sales = [
            row for row in reader
            if float(row["amount"]) > 100
        ]
    by_region = Counter(sale["region"] for sale in sales)
    top_regions = by_region.most_common(5)
    return top_regions
#![allow(unused)]
fn main() {
// Rust — цепочки итераторов с сильной типизацией
use std::collections::HashMap;

#[derive(Debug, serde::Deserialize)]
struct Sale {
    region: String,
    amount: f64,
}

fn analyze_sales(filename: &str) -> Vec<(String, usize)> {
    let data = std::fs::read_to_string(filename).unwrap();
    let mut reader = csv::Reader::from_reader(data.as_bytes());

    let mut by_region: HashMap<String, usize> = HashMap::new();
    for sale in reader.deserialize::<Sale>().flatten() {
        if sale.amount > 100.0 {
            *by_region.entry(sale.region).or_insert(0) += 1;
        }
    }

    let mut top: Vec<_> = by_region.into_iter().collect();
    top.sort_by(|a, b| b.1.cmp(&a.1));
    top.truncate(5);
    top
}
}

Глобальная конфигурация / синглтон

# Python — синглтон на уровне модуля (распространённый паттерн)
# config.py
import json

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            with open("config.json") as f:
                cls._instance.data = json.load(f)
        return cls._instance

config = Config()  # Синглтон на уровне модуля
#![allow(unused)]
fn main() {
// Rust — OnceLock для ленивой статической инициализации (Rust 1.70+)
use std::sync::OnceLock;
use serde_json::Value;

static CONFIG: OnceLock<Value> = OnceLock::new();

fn get_config() -> &'static Value {
    CONFIG.get_or_init(|| {
        let data = std::fs::read_to_string("config.json")
            .expect("Не удалось прочитать конфигурацию");
        serde_json::from_str(&data)
            .expect("Не удалось разобрать конфигурацию")
    })
}

// Использование в любом месте:
let db_host = get_config()["database"]["host"].as_str().unwrap();
}

Основные крейты для разработчиков на Python

Обработка данных и сериализация

ЗадачаPythonКрейт RustПримечания
JSONjsonserde_jsonТипобезопасная сериализация
CSVcsv, pandascsvПотоковая обработка, мало памяти
YAMLpyyamlserde_yamlФайлы конфигурации
TOMLtomllibtomlФайлы конфигурации
Валидация данныхpydanticserde + собственная логикаПроверка на этапе компиляции
Дата и времяdatetimechronoПолная поддержка часовых поясов
Регулярные выраженияreregexОчень быстро
UUIDuuiduuidТа же концепция

Веб и сеть

ЗадачаPythonКрейт RustПримечания
HTTP-клиентrequestsreqwestАсинхронный в первую очередь
Веб-фреймворкFastAPI/Flaskaxum / actix-webОчень быстро
WebSocketwebsocketstokio-tungsteniteАсинхронный
gRPCgrpciotonicПолная поддержка
База данных (SQL)sqlalchemysqlx / dieselSQL с проверкой на этапе компиляции
Redisredis-pyredisПоддержка async

CLI и система

ЗадачаPythonКрейт RustПримечания
Аргументы CLIargparse/clickclapМакросы derive
Цветной выводcoloramacoloredЦвета в терминале
Индикатор прогрессаtqdmindicatifПохожий UX
Отслеживание файловwatchdognotifyКроссплатформенный
ЛогированиеloggingtracingСтруктурированный, готов к async
Переменные окруженияos.environstd::env + dotenvyПоддержка .env
Подпроцессыsubprocessstd::process::CommandВстроен
Временные файлыtempfiletempfileОдинаковое название!

Тестирование

ЗадачаPythonКрейт RustПримечания
Фреймворк тестовpytestВстроенный + rstestcargo test
Мокиunittest.mockmockallНа основе трейтов
Property-тестированиеhypothesisproptestПохожий API
Снапшот-тестированиеsyrupyinstaПодтверждение снимков
Бенчмаркиpytest-benchmarkcriterionСтатистические
Покрытие кодаcoverage.pycargo-tarpaulinНа основе LLVM

Стратегия постепенного внедрения

flowchart TB
    A["1️⃣ Профилируйте Python<br/>(найдите узкие места)"] --> B["2️⃣ Напишите расширение на Rust<br/>(PyO3 + maturin)"]
    B --> C["3️⃣ Замените вызов Python<br/>(тот же API)"]
    C --> D["4️⃣ Расширяйте постепенно<br/>(больше функций)"]
    D --> E{"Полная переписка<br/>оправдана?"}
    E -->|Да| F["Чистый Rust🦀"]
    E -->|Нет| G["Гибрид🐍+🦀"]
    style A fill:#ffeeba
    style B fill:#fff3cd
    style C fill:#d4edda
    style D fill:#d4edda
    style F fill:#c3e6cb
    style G fill:#c3e6cb

📌 См. также: Гл. 14 — Unsafe Rust и FFI описывает низкоуровневые детали FFI, необходимые для привязок PyO3.

Шаг 1: найдите узкие места

# Сначала профилируйте код на Python
import cProfile
cProfile.run('main()')  # Находим функции, которые сильно грузят CPU

# Или используйте py-spy как профилировщик сэмплирования:
# py-spy top --pid <python-pid>
# py-spy record -o profile.svg -- python main.py

Шаг 2: напишите расширение на Rust для узкого места

# Создаём расширение на Rust с помощью maturin
cd my_python_project
maturin init --bindings pyo3

# Пишем горячую функцию на Rust (см. раздел про PyO3 выше)
# Собираем и устанавливаем:
maturin develop --release

Шаг 3: замените вызов Python вызовом Rust

# До:
result = python_hot_function(data)  # Медленно

# После:
import my_rust_extension
result = my_rust_extension.hot_function(data)  # Быстро!

# Тот же API, те же тесты, в 10–100 раз быстрее

Шаг 4: расширяйте постепенно

#![allow(unused)]
fn main() {
Неделя 1–2: заменить одну функцию, ограниченную CPU, на Rust
Неделя 3–4: заменить слой разбора и валидации данных
Месяц 2:    заменить основной конвейер данных
Месяц 3+:   рассмотреть полную переписку на Rust, если выгода оправдывает затраты

Ключевой принцип: оставьте Python для оркестрации, а Rust для вычислений.
}

💼 Кейс: ускорение конвейера данных с PyO3

Финтех-стартап обрабатывает ежедневно файлы CSV с транзакциями общим объёмом 2 ГБ на Python. Критическое узкое место — шаг валидации и преобразования:

# Python — медленная часть (~12 минут на 2 ГБ)
import csv
from decimal import Decimal
from datetime import datetime

def validate_and_transform(filepath: str) -> list[dict]:
    results = []
    with open(filepath) as f:
        reader = csv.DictReader(f)
        for row in reader:
            # Разбираем и проверяем каждое поле
            amount = Decimal(row["amount"])
            if amount < 0:
                raise ValueError(f"Отрицательная сумма: {amount}")
            date = datetime.strptime(row["date"], "%Y-%m-%d")
            category = categorize(row["merchant"])  # Поиск по строкам, ~50 правил

            results.append({
                "amount_cents": int(amount * 100),
                "date": date.isoformat(),
                "category": category,
                "merchant": row["merchant"].strip().lower(),
            })
    return results
# ~12 минут на 15 млн строк. Пробовали pandas: вышло ~8 минут, но 6 ГБ ОЗУ.

Шаг 1: профилируем и находим узкое место (разбор CSV + преобразование Decimal + поиск по строкам = 95% времени).

Шаг 2: пишем расширение на Rust:

#![allow(unused)]
fn main() {
// src/lib.rs — расширение PyO3
use pyo3::prelude::*;
use pyo3::types::PyList;
use std::fs::File;
use std::io::BufReader;

#[derive(Debug)]
struct Transaction {
    amount_cents: i64,
    date: String,
    category: String,
    merchant: String,
}

fn categorize(merchant: &str) -> &'static str {
    // Aho-Corasick или простые правила: компилируются один раз и работают очень быстро
    if merchant.contains("amazon") { "shopping" }
    else if merchant.contains("uber") || merchant.contains("lyft") { "transport" }
    else if merchant.contains("starbucks") { "food" }
    else { "other" }
}

#[pyfunction]
fn process_transactions(path: &str) -> PyResult<Vec<(i64, String, String, String)>> {
    let file = File::open(path).map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
    let mut reader = csv::Reader::from_reader(BufReader::new(file));

    let mut results = Vec::with_capacity(15_000_000); // Предварительное выделение памяти

    for record in reader.records() {
        let record = record.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
        let amount_str = &record[0];
        let amount_cents = parse_amount_cents(amount_str)?;  // Ваш собственный парсер (Decimal не нужен)
        let date = &record[1];  // Уже в формате ISO, нужно только проверить
        let merchant = record[2].trim().to_lowercase();
        let category = categorize(&merchant).to_string();

        results.push((amount_cents, date.to_string(), category, merchant));
    }
    Ok(results)
}

#[pymodule]
fn fast_pipeline(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(process_transactions, m)?)?;
    Ok(())
}
}

Шаг 3: заменяем одну строку в Python:

# До:
results = validate_and_transform("transactions.csv")  # 12 минут

# После:
import fast_pipeline
results = fast_pipeline.process_transactions("transactions.csv")  # 45 секунд

# Та же оркестрация на Python, те же тесты, то же развёртывание
# Заменена только одна функция

Результаты:

МетрикаPython (csv + Decimal)Rust (PyO3 + крейт csv)
Время (2 ГБ / 15 млн строк)12 минут45 секунд
Пиковая память6 ГБ (pandas) / 2 ГБ (csv)200 МБ
Строк изменено в Python—1 (import + вызов)
Написано кода на Rust—~60 строк
Пройденные тесты47/4747/47 (без изменений)

Ключевой урок: переписывать всё приложение не нужно. Найдите 5% кода, на который уходит 95% времени, перепишите его на Rust с PyO3, а всё остальное оставьте на Python. Команда перешла от «нам нужно добавить серверов» к «одного сервера достаточно».


Упражнения

🏋️ Упражнение: матрица решений по миграции (нажмите, чтобы раскрыть)

Задание: у вас есть веб-приложение на Python с такими компонентами. Для каждого решите: оставить на Python, переписать на Rust или мост PyO3. Обоснуйте каждый выбор.

  1. Обработчики маршрутов Flask (разбор запросов, ответы JSON)
  2. Генерация миниатюр изображений (ограничено CPU, 10 тыс. изображений в день)
  3. Запросы ORM к базе данных (SQLAlchemy)
  4. Разбор CSV для финансовых файлов по 2 ГБ (запускается ночью)
  5. Панель администратора (шаблоны Jinja2)
🔑 Решение
КомпонентРешениеОбоснование
Обработчики маршрутов Flask🐍 Оставить на PythonОграничено вводом-выводом, завязано на фреймворк, выигрыш от Rust мал
Генерация миниатюр изображений🦀 Мост PyO3Горячий путь, ограниченный CPU: API остаётся на Python, внутренности на Rust
Запросы ORM к базе данных🐍 Оставить на PythonSQLAlchemy зрелый, запросы ограничены вводом-выводом
Разбор CSV (2 ГБ)🦀 Мост PyO3 или полный RustОграничено CPU и памятью, здесь хорошо работает разбор без копирования
Панель администратора🐍 Оставить на PythonКод UI и шаблонов, производительность не проблема

Ключевой вывод: золотая середина миграции — это код, ограниченный CPU и критичный к производительности, с чистой границей. Не переписывайте клеящий код и обработчики, ограниченные вводом-выводом: выигрыш не окупит затрат.