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

Rust для программистов на Python: полное учебное руководство

Подробное руководство по изучению Rust для разработчиков, которые уже знакомы с Python. Книга охватывает всё: от базового синтаксиса до продвинутых паттернов. Основной акцент сделан на концептуальных сдвигах, которые происходят при переходе от языка с динамической типизацией и сборщиком мусора к системному языку со статической типизацией и проверкой безопасности памяти на этапе компиляции.

Как пользоваться этой книгой

Формат самостоятельного изучения: сначала пройдите часть I (гл. 1–6), она тесно связана с понятиями Python, которые вы уже знаете. Часть II (гл. 7–12) знакомит с идеями, специфичными для Rust, такими как владение и трейты. Часть III (гл. 13–16) посвящена продвинутым темам и миграции.

Рекомендации по темпу:

ГлавыТемаПримерное времяКонтрольная точка
1–4Настройка, типы, управление потоком выполнения1 деньВы можете написать CLI-конвертер температур на Rust
5–6Структуры данных, перечисления, сопоставление с образцом1–2 дняВы можете определить перечисление с данными и исчерпывающе разобрать его через match
7Владение и заимствование1–2 дняВы можете объяснить, почему let s2 = s1 делает s1 недействительным
8–9Модули, обработка ошибок1 деньВы можете создать многофайловый проект, который передаёт ошибки дальше с помощью ?
10–12Трейты, обобщения, замыкания, итераторы1–2 дняВы можете перевести list comprehension в цепочку итераторов
13Конкурентность1 деньВы можете написать потокобезопасный счётчик с Arc<Mutex<T>>
14Unsafe, PyO3, тестирование1 деньВы можете вызвать функцию Rust из Python через PyO3
15–16Миграция, лучшие практикиВ своём темпеСправочный материал: обращайтесь к нему при написании реального кода
17Итоговый проект2–3 дняСоздайте полноценное CLI-приложение, которое объединяет всё изученное

Как работать с упражнениями:

  • В главах есть практические упражнения в сворачиваемых блоках <details> с решениями
  • Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение. Борьба с borrow checker — часть обучения, а сообщения компилятора — ваш учитель
  • Если вы застряли больше чем на 15 минут, раскройте решение, изучите его, затем закройте и попробуйте снова с нуля
  • Rust Playground позволяет запускать код без локальной установки

Индикаторы сложности:

  • 🟢 Начальный: прямой перевод понятий из Python
  • 🟡 Средний: требует понимания владения или трейтов
  • 🔴 Продвинутый: времена жизни, внутреннее устройство async или unsafe-код

Если вы упёрлись в стену:

  • Внимательно читайте сообщение компилятора: ошибки Rust исключительно полезны
  • Перечитайте нужный раздел. Концепции вроде владения (гл. 7) часто становятся понятны при втором прочтении
  • Документация стандартной библиотеки Rust отличная: ищите любой тип или метод
  • Более глубокие async-паттерны описаны в сопутствующем курсе Async Rust

Содержание

Часть I — Основы

1. Введение и мотивация 🟢

2. Начало работы 🟢

3. Встроенные типы и переменные 🟢

4. Управление потоком выполнения 🟢

5. Структуры данных и коллекции 🟢

6. Перечисления и сопоставление с образцом 🟡

Часть II — Ключевые концепции

7. Владение и заимствование 🟡

8. Крейты и модули 🟢

9. Обработка ошибок 🟡

10. Трейты и обобщения 🟡

11. Трейты From и Into 🟡

12. Замыкания и итераторы 🟡

Часть III — Продвинутые темы и миграция

13. Конкурентность 🔴

14. Unsafe Rust, FFI и тестирование 🔴

15. Паттерны миграции 🟡

16. Лучшие практики 🟡


Часть IV — Итоговый проект

17. Итоговый проект: CLI-менеджер задач 🔴


1. Введение и мотивация

О курсе и общий подход

  • О курсе
    • Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0)
    • Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus
  • Курс задуман максимально интерактивным
    • Предполагается, что вы знаете Python и его экосистему
    • Примеры намеренно сопоставляют понятия Python с их аналогами в Rust
    • Не стесняйтесь задавать уточняющие вопросы в любой момент

Аргументы в пользу Rust для разработчиков на Python

Что вы узнаете: почему разработчики на Python переходят на Rust, какие реальные выигрыши в производительности получили компании (Dropbox, Discord, Pydantic), когда Rust — правильный выбор по сравнению с Python и в чём принципиальные философские различия между двумя языками.

Сложность: 🟢 Начальный

Производительность: от минут до миллисекунд

Python известен медленной работой с задачами, ограниченными процессором. Rust даёт производительность уровня C при высокоуровневом стиле написания кода.

# Python — ~2 секунды на 10 миллионов вызовов
import time

def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

start = time.perf_counter()
results = [fibonacci(n % 30) for n in range(10_000_000)]
elapsed = time.perf_counter() - start
print(f"Прошло: {elapsed:.2f}s")  # ~2 с на типичном железе
// Rust — ~0.07 секунды на те же 10 миллионов вызовов
use std::time::Instant;

fn fibonacci(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 2..=n {
        let temp = b;
        b = a + b;
        a = temp;
    }
    b
}

fn main() {
    let start = Instant::now();
    let results: Vec<u64> = (0..10_000_000).map(|n| fibonacci(n % 30)).collect();
    println!("Прошло: {:.2?}", start.elapsed());  // ~0.07 с
}

Примечание: для честного сравнения производительности Rust нужно запускать в release-режиме (cargo run --release). В чём разница? Python при каждом + обращается к поиску в словаре, извлекает целые числа из объектов в куче и проверяет типы на каждой операции. Rust компилирует fibonacci напрямую в несколько инструкций x86 add/mov — ровно тот код, который выдал бы компилятор C.

Безопасность памяти без сборщика мусора

У сборщика мусора Python на основе подсчёта ссылок есть известные проблемы: циклические ссылки, непредсказуемое время вызова __del__ и фрагментация памяти. Rust устраняет их на этапе компиляции.

# Python — циклическая ссылка, которую счётчик ссылок CPython не может освободить
class Node:
    def __init__(self, value):
        self.value = value
        self.parent = None
        self.children = []

    def add_child(self, child):
        self.children.append(child)
        child.parent = self  # Циклическая ссылка!

# Эти два узла ссылаются друг на друга — счётчик ссылок никогда не станет 0.
# Детектор циклов CPython *в конце концов* их очистит,
# но вы не контролируете, когда это произойдёт, и это добавляет накладные расходы на паузы GC.
root = Node("root")
child = Node("child")
root.add_child(child)
// Rust — владение по замыслу исключает циклические ссылки
struct Node {
    value: String,
    children: Vec<Node>,  // Дети ПРИНАДЛЕЖАТ родителю — циклы невозможны
}

impl Node {
    fn new(value: &str) -> Self {
        Node {
            value: value.to_string(),
            children: Vec::new(),
        }
    }

    fn add_child(&mut self, child: Node) {
        self.children.push(child);  // Владение переходит здесь
    }
}

fn main() {
    let mut root = Node::new("root");
    let child = Node::new("child");
    root.add_child(child);
    // Когда root уничтожается, уничтожаются и все дети.
    // Детерминированно, без накладных расходов, без GC.
}

Ключевая мысль: в Rust потомок не хранит ссылку обратно на родителя. Если действительно нужны перекрёстные ссылки (например, для графа), используются явные механизмы вроде Rc<RefCell<T>> или индексов, что делает сложность видимой и осознанной.


Частые проблемы Python, которые решает Rust

1. Ошибки типов во время выполнения

Самая распространённая продакшен-ошибка в Python: передача функции значения неправильного типа. Подсказки типов помогают, но они не проверяются.

# Python — подсказки типов являются рекомендациями, а не правилами
def process_user(user_id: int, name: str) -> dict:
    return {"id": user_id, "name": name.upper()}

# Все эти вызовы «проходят» на месте вызова и падают во время выполнения
process_user("not-a-number", 42)        # TypeError: int has no .upper()
process_user(None, "Alice")             # Молча сохраняет None как id — ошибка спрячется, пока последующий код не ожидает int

# Даже с mypy типы можно обойти:
data = json.loads('{"id": "oops"}')     # Всегда возвращает Any
process_user(data["id"], data["name"])  # mypy это не поймает
#![allow(unused)]
fn main() {
// Rust — компилятор ловит всё это до запуска программы
fn process_user(user_id: i64, name: &str) -> User {
    User {
        id: user_id,
        name: name.to_uppercase(),
    }
}

// process_user("not-a-number", 42);     // ❌ Ошибка компиляции: ожидался i64, найден &str
// process_user(None, "Alice");           // ❌ Ошибка компиляции: ожидался i64, найден Option
// Лишние аргументы — всегда ошибка компиляции.

// Десериализация JSON тоже типобезопасна:
#[derive(Deserialize)]
struct UserInput {
    id: i64,     // Должно быть числом в JSON
    name: String, // Должно быть строкой в JSON
}
let input: UserInput = serde_json::from_str(json_str)?; // Возвращает Err, если типы не совпадают
process_user(input.id, &input.name); // ✅ Типы гарантированно корректны
}

2. None: миллиардная ошибка (издание для Python)

None может появиться там, где ожидается значение. В Python нет способа на этапе компиляции предотвратить AttributeError: 'NoneType' object has no attribute ....

# Python — None проникает повсюду
def find_user(user_id: int) -> dict | None:
    users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
    return users.get(user_id)

user = find_user(999)         # Возвращает None
print(user["name"])           # 💥 TypeError: 'NoneType' object is not subscriptable

# Даже с подсказкой типа Optional ничто не требует проверки:
from typing import Optional
def get_name(user_id: int) -> Optional[str]:
    return None

name: Optional[str] = get_name(1)
print(name.upper())          # 💥 AttributeError — mypy предупреждает, а во время выполнения это никого не волнует
#![allow(unused)]
fn main() {
// Rust — None невозможен, пока вы его явно не обработаете
fn find_user(user_id: i64) -> Option<User> {
    let users = HashMap::from([
        (1, User { name: "Alice".into() }),
        (2, User { name: "Bob".into() }),
    ]);
    users.get(&user_id).cloned()
}

let user = find_user(999);  // Возвращает вариант None типа Option<User>
// println!("{}", user.name);  // ❌ Ошибка компиляции: у Option<User> нет поля `name`

// Вариант None ОБЯЗАТЕЛЬНО нужно обработать:
match find_user(999) {
    Some(user) => println!("{}", user.name),
    None => println!("Пользователь не найден"),
}

// Или используйте комбинаторы:
let name = find_user(999)
    .map(|u| u.name)
    .unwrap_or_else(|| "Unknown".to_string());
}

3. GIL: потолок конкурентности Python

Глобальная блокировка интерпретатора (GIL) в Python означает, что потоки не выполняют код Python параллельно. threading полезен только для задач, ограниченных вводом-выводом; задачи, ограниченные процессором, требуют multiprocessing (с накладными расходами на сериализацию) или C-расширений.

# Python — потоки НЕ ускоряют работу с процессором из-за GIL
import threading
import time

def cpu_work(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

start = time.perf_counter()
threads = [threading.Thread(target=cpu_work, args=(10_000_000,)) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()
elapsed = time.perf_counter() - start
print(f"4 потока: {elapsed:.2f}s")  # Примерно ТАКОЕ ЖЕ время, как у 1 потока! GIL не даёт параллелизму работать.

# multiprocessing «работает», но сериализует данные между процессами:
from multiprocessing import Pool
with Pool(4) as p:
    results = p.map(cpu_work, [10_000_000] * 4)  # ~в 4 раза быстрее, но с накладными расходами на pickle
// Rust — настоящий параллелизм, без GIL и без накладных расходов на сериализацию
use std::thread;

fn cpu_work(n: u64) -> u64 {
    (0..n).map(|i| i * i).sum()
}

fn main() {
    let start = std::time::Instant::now();
    let handles: Vec<_> = (0..4)
        .map(|_| thread::spawn(|| cpu_work(3_000_000)))
        .collect();

    let results: Vec<u64> = handles.into_iter()
        .map(|h| h.join().unwrap())
        .collect();

    println!("4 потока: {:.2?}", start.elapsed());  // ~в 4 раза быстрее одного потока
}

С Rayon (библиотекой параллельных итераторов для Rust) параллелизм становится ещё проще:

#![allow(unused)]
fn main() {
use rayon::prelude::*;
let results: Vec<u64> = inputs.par_iter().map(|&n| cpu_work(n)).collect();
}

4. Боль развёртывания и распространения

Развёртывание Python известно своей сложностью: venv, конфликты с системным Python, сбои pip install, колёса (wheels) C-расширений, Docker-образы с полным рантаймом Python.

# Чек-лист развёртывания Python:
# 1. Какая версия Python? 3.9? 3.10? 3.11? 3.12?
# 2. Виртуальное окружение: venv, conda, poetry, pipenv?
# 3. C-расширения: нужен компилятор? manylinux-колёса?
# 4. Системные зависимости: libssl, libffi и т. д.?
# 5. Docker: полный образ python:3.12 весит 1,0 ГБ
# 6. Время запуска: 200–500 мс для приложений с большим количеством импортов

# Docker-образ: ~1 ГБ
# FROM python:3.12-slim
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["python", "app.py"]
#![allow(unused)]
fn main() {
// Развёртывание Rust: один статический бинарник, рантайм не нужен
// cargo build --release → один бинарник, ~5–20 МБ
// Копируйте куда угодно — ни Python, ни venv, ни зависимостей

// Docker-образ: ~5 МБ (from scratch или distroless)
// FROM scratch
// COPY target/release/my_app /my_app
// CMD ["/my_app"]

// Время запуска: <1 мс
// Кросс-компиляция: cargo build --target x86_64-unknown-linux-musl
}

Когда выбирать Rust вместо Python

Выбирайте Rust, когда:

  • Производительность критична: конвейеры данных, обработка в реальном времени, вычислительно тяжёлые сервисы
  • Корректность важна: финансовые системы, код, критичный к безопасности, реализации протоколов
  • Простота развёртывания: один бинарник, без зависимостей рантайма
  • Низкоуровневый контроль: взаимодействие с железом, интеграция с ОС, встраиваемые системы
  • Настоящая конкурентность: параллелизм для задач, ограниченных процессором, без обходных путей вокруг GIL
  • Эффективность памяти: снижение облачных расходов для сервисов, которые много используют память
  • Долгоработающие сервисы: где важна предсказуемая задержка (без пауз GC)

Оставайтесь на Python, когда:

  • Быстрое прототипирование: исследовательский анализ данных, скрипты, разовые инструменты
  • Рабочие процессы ML/AI: экосистема PyTorch, TensorFlow, scikit-learn
  • Клеящий код: подключение API, скрипты преобразования данных
  • Экспертиза команды: когда кривая обучения Rust не окупается выгодами
  • Время выхода на рынок: когда скорость разработки важнее скорости выполнения
  • Интерактивная работа: блокноты Jupyter, разработка через REPL
  • Скрипты: автоматизация, задачи системного администрирования, быстрые утилиты

Рассмотрите оба варианта (гибридный подход с PyO3):

  • Вычислительно тяжёлый код на Rust: вызывается из Python через PyO3/maturin
  • Бизнес-логика и оркестрация на Python: привычно и продуктивно
  • Постепенная миграция: найти узкие места и заменить их расширениями на Rust
  • Лучшее из двух миров: экосистема Python + производительность Rust

Практический эффект: почему компании выбирают Rust

Dropbox: инфраструктура хранения

  • До (Python): высокая загрузка CPU, избыточное потребление памяти в движке синхронизации
  • После (Rust): прирост производительности в 10 раз, снижение потребления памяти на 50%
  • Результат: миллионы долларов экономии на инфраструктуре

Discord: бэкенд голоса и видео

  • До (Python → Go): паузы GC приводили к обрывам звука
  • После (Rust): стабильно низкая задержка
  • Результат: лучше пользовательский опыт и ниже расходы на серверы

Cloudflare: edge-воркеры

  • Почему Rust: компиляция в WebAssembly, предсказуемая производительность на периферии сети
  • Результат: воркеры запускаются с холодным стартом в микросекундах

Pydantic V2

  • До: валидация на чистом Python — медленно для больших объёмов данных
  • После: ядро на Rust (через PyO3) — валидация в 5–50 раз быстрее
  • Результат: тот же API на Python, значительно более быстрое выполнение

Почему это важно для разработчиков на Python:

  1. Взаимодополняющие навыки: Rust и Python решают разные задачи
  2. Мост PyO3: пишите расширения на Rust, которые можно вызывать из Python
  3. Понимание производительности: вы узнаете, почему Python медленный и как исправлять узкие места
  4. Карьерный рост: экспертиза в системном программировании становится всё ценнее
  5. Облачные расходы: код в 10 раз быстрее = значительно меньше расходов на инфраструктуру

Сравнение языковой философии

Философия Python

  • Читаемость важна: чистый синтаксис, «один очевидный способ сделать это»
  • Всё в комплекте: обширная стандартная библиотека, быстрое прототипирование
  • Утиная типизация: «Если это ходит как утка и крякает как утка…»
  • Скорость разработки: оптимизация под скорость написания кода, а не скорость выполнения
  • Всё динамически: изменение классов во время выполнения, monkey-patching, метаклассы

Философия Rust

  • Производительность без компромиссов: абстракции с нулевой стоимостью, без накладных расходов рантайма
  • Корректность прежде всего: если код компилируется, целые классы ошибок становятся невозможными
  • Явное лучше неявного: никакого скрытого поведения и неявных преобразований
  • Владение: у ресурсов ровно один владелец: память, файлы, сокеты
  • Конкурентность без страха: система типов предотвращает гонки данных на этапе компиляции
graph LR
    subgraph PY["🐍 Python"]
        direction TB
        PY_CODE["Ваш код"] --> PY_INTERP["Интерпретатор — виртуальная машина CPython"]
        PY_INTERP --> PY_GC["Сборщик мусора — подсчёт ссылок + GC"]
        PY_GC --> PY_GIL["GIL — нет настоящего параллелизма"]
        PY_GIL --> PY_OS["ОС / Железо"]
    end

    PY ~~~ RS
    subgraph RS["🦀 Rust"]
        direction TB
        RS_CODE["Ваш код"] --> RS_NONE["Нет накладных расходов рантайма"]
        RS_NONE --> RS_OWN["Владение — на этапе компиляции, без затрат"]
        RS_OWN --> RS_THR["Нативные потоки — настоящий параллелизм"]
        RS_THR --> RS_OS["ОС / Железо"]
    end

    style PY_INTERP fill:#fff3e0,color:#000,stroke:#e65100
    style PY_GC fill:#fff3e0,color:#000,stroke:#e65100
    style PY_GIL fill:#ffcdd2,color:#000,stroke:#c62828
    style RS_NONE fill:#c8e6c9,color:#000,stroke:#2e7d32
    style RS_OWN fill:#c8e6c9,color:#000,stroke:#2e7d32
    style RS_THR fill:#c8e6c9,color:#000,stroke:#2e7d32

Краткая справка: Rust против Python

КонцепцияPythonRustКлючевое отличие
ТипизацияДинамическая (duck typing)Статическая (на этапе компиляции)Ошибки обнаруживаются до выполнения
ПамятьСборка мусора (подсчёт ссылок + GC для циклов)Система владенияБез накладных расходов, детерминированная очистка
None/nullNone где угодноOption<T>Безопасность None на этапе компиляции
Обработка ошибокraise/try/exceptResult<T, E>Явно, без скрытого потока управления
ИзменяемостьВсё изменяемоНеизменяемость по умолчаниюИзменение нужно включать явно
СкоростьИнтерпретируемый (~в 10–100 раз медленнее)Компилируемый (скорость C/C++)На порядки быстрее
КонкурентностьGIL ограничивает потокиНет GIL, трейты Send/SyncНастоящий параллелизм по умолчанию
Зависимостиpip install / poetry addcargo addУправление зависимостями встроено
Система сборкиsetuptools/poetry/hatchCargoОдин единый инструмент
Упаковкаpyproject.tomlCargo.tomlПохожая декларативная конфигурация
REPLИнтерактивный pythonНет REPL (используйте тесты или cargo run)Процесс, ориентированный на компиляцию
Подсказки типовНеобязательные, не проверяютсяОбязательные, проверяются компиляторомТипы — не украшение

Упражнения

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

Задание: для каждого фрагмента на Python предскажите, что в Rust потребуется иначе. Код писать не нужно, просто опишите ограничение.

  1. x = [1, 2, 3]; y = x; x.append(4) — что произойдёт в Rust?
  2. data = None; print(data.upper()) — как Rust этому помешает?
  3. import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start() — что требует Rust?
🔑 Решение
  1. Перемещение владения: let y = x; перемещает x, поэтому x.push(4) — ошибка компиляции. Понадобилось бы let y = x.clone(); или заимствование через let y = &x;.
  2. Нет null: data не может быть None, если только это не Option<String>. Нужно использовать match, .unwrap() или if let, и неожиданных ошибок NoneType не будет.
  3. Send + Sync: компилятор требует обернуть shared в Arc<Mutex<Vec<i32>>>. Забытая блокировка — это ошибка компиляции, а не гонка данных.

Главный вывод: Rust переносит ошибки времени выполнения в ошибки времени компиляции. «Трение», которое вы чувствуете, — это компилятор, который ловит настоящие баги.


2. Начало работы

Установка и настройка

Что вы узнаете: как установить Rust и его тулчейн, чем система сборки Cargo отличается от pip/Poetry, как настроить IDE, как написать первую программу Hello, world! и какие ключевые слова Rust соответствуют конструкциям Python.

Сложность: 🟢 Начальный

Установка Rust

# Установка Rust через rustup (Linux/macOS/WSL)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Проверка установки
rustc --version     # Компилятор Rust
cargo --version     # Инструмент сборки + менеджер пакетов (как pip и setuptools вместе)

# Обновление Rust
rustup update

Инструменты Rust и инструменты Python

НазначениеPythonRust
Среда выполнения языкаpython (интерпретатор)rustc (компилятор, напрямую вызывается редко)
Менеджер пакетовpip / poetry / uvcargo (встроен)
Конфигурация проектаpyproject.tomlCargo.toml
Lock-файлpoetry.lock / requirements.txtCargo.lock
Виртуальное окружениеvenv / condaНе нужно (зависимости хранятся для каждого проекта)
Форматтерblack / ruff formatrustfmt (встроен: cargo fmt)
Линтерruff / flake8 / pylintclippy (встроен: cargo clippy)
Проверка типовmypy / pyrightВстроена в компилятор (всегда включена)
Запуск тестовpytestcargo test (встроен)
Документацияsphinx / mkdocscargo doc (встроен)
REPLpython / ipythonНет (используйте cargo test или Rust Playground)

Настройка IDE

VS Code (рекомендуется):

Расширения для установки:
- rust-analyzer        ← Обязательно: возможности IDE, подсказки типов, автодополнение
- Even Better TOML     ← Подсветка синтаксиса для Cargo.toml
- CodeLLDB             ← Поддержка отладчика

# Соответствие с Python:
# rust-analyzer ≈ Pylance (но с полным покрытием типами всегда)
# cargo clippy  ≈ ruff (но проверяет корректность, а не только стиль)

Ваша первая программа на Rust

Hello World на Python

# hello.py — просто запустите
print("Hello, World!")

# Запуск:
# python hello.py

Hello World на Rust

// src/main.rs — сначала нужно скомпилировать
fn main() {
    println!("Hello, World!");   // println! — это макрос (обратите внимание на !)
}

// Сборка и запуск:
// cargo run

Ключевые различия для разработчиков на Python

Python:                              Rust:
─────────                            ─────
- main() не нужен                    - fn main() — точка входа
- Отступы = блоки                    - Фигурные скобки {} = блоки
- print() — это функция              - println!() — это макрос (важен восклицательный знак)
- Без точек с запятой                - Точка с запятой завершает инструкции
- Без объявлений типов               - Типы выводятся, но всегда известны
- Интерпретируется (запуск напрямую) - Компилируется (cargo build, затем запуск)
- Ошибки во время выполнения         - Большинство ошибок на этапе компиляции

Создание первого проекта

# Python                              # Rust
mkdir myproject                        cargo new myproject
cd myproject                           cd myproject
python -m venv .venv                   # Виртуальное окружение не нужно
source .venv/bin/activate              # Активация не нужна
# Создаём файлы вручную               # src/main.rs уже создан

# Структура проекта Python:            Структура проекта Rust:
# myproject/                           myproject/
# ├── pyproject.toml                   ├── Cargo.toml        (аналог pyproject.toml)
# ├── src/                             ├── src/
# │   └── myproject/                   │   └── main.rs       (точка входа)
# │       ├── __init__.py              └── (__init__.py не нужен)
# │       └── main.py
# └── tests/
#     └── test_main.py
graph LR
    subgraph Python ["Проект Python"]
        PP["pyproject.toml"] --- PS["src/"]
        PS --- PM["myproject/"]
        PM --- PI["__init__.py"]
        PM --- PMN["main.py"]
        PP --- PT["tests/"]
    end
    subgraph Rust ["Проект Rust"]
        RC["Cargo.toml"] --- RS["src/"]
        RS --- RM["main.rs"]
        RC --- RTG["target/ (генерируется автоматически)"]
    end
    Python ~~~ Rust
    style Python fill:#ffeeba
    style Rust fill:#d4edda

Ключевое отличие: проекты на Rust проще. Не нужны __init__.py, виртуальные окружения и путаница между setup.py, setup.cfg и pyproject.toml. Достаточно Cargo.toml и src/.


Cargo и pip/Poetry

Конфигурация проекта

# Python — pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
# Rust — Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"          # Редакция Rust (аналог версии Python)

[dependencies]
reqwest = "0.12"          # HTTP-клиент (аналог requests)
serde = { version = "1.0", features = ["derive"] }  # Сериализация (аналог pydantic)

[dev-dependencies]
# Тестовые зависимости — собираются только для `cargo test`
# (Отдельная конфигурация для тестов не нужна — `cargo test` встроен)

Частые команды Cargo

# Аналог в Python                  # Rust
pip install requests               cargo add reqwest
pip install -r requirements.txt    cargo build           # автоматически устанавливает зависимости
pip install -e .                   cargo build            # всегда «редактируемая» сборка
python -m pytest                   cargo test
python -m mypy .                   # Встроено в компилятор — работает всегда
ruff check .                       cargo clippy
ruff format .                      cargo fmt
python main.py                     cargo run
python -c "..."                    # Аналога нет — используйте cargo run или тесты

# Специфичное для Rust:
cargo new myproject                # Создать новый проект
cargo build --release              # Оптимизированная сборка (в 10–100 раз быстрее отладочной)
cargo doc --open                   # Сгенерировать и открыть документацию API
cargo update                       # Обновить зависимости (аналог pip install --upgrade)

Ключевые слова Rust для разработчиков на Python

Ключевые слова для переменных и изменяемости

#![allow(unused)]
fn main() {
// let — объявление переменной (как присваивание в Python, но неизменяемой по умолчанию)
let name = "Alice";          // Python: name = "Alice" (но изменяемая)
// name = "Bob";             // ❌ Ошибка компиляции! По умолчанию неизменяемая

// mut — явное включение изменяемости
let mut count = 0;           // Python: count = 0 (в Python всегда изменяемая)
count += 1;                  // ✅ Разрешено благодаря `mut`

// const — константа времени компиляции (как соглашение Python об UPPER_CASE, но с принудительной проверкой)
const MAX_SIZE: usize = 1024;   // Python: MAX_SIZE = 1024 (только соглашение)

// static — глобальная переменная (используйте осторожно; в Python есть глобальные переменные уровня модуля)
static VERSION: &str = "1.0";
}

Ключевые слова владения и заимствования

#![allow(unused)]
fn main() {
// У этих ключевых слов НЕТ аналогов в Python — это специфичные для Rust концепции

// & — заимствование (ссылка только для чтения)
fn print_name(name: &str) { }    // Python: def print_name(name: str) — но Python всегда передаёт по ссылке

// &mut — изменяемое заимствование
fn append(list: &mut Vec<i32>) { }  // Python: def append(lst: list) — в Python всегда изменяемый

// move — передача владения (в Rust происходит неявно, в Python — никогда)
let s1 = String::from("hello");
let s2 = s1;    // s1 ПЕРЕМЕЩЁН в s2 — s1 больше не действителен
// println!("{}", s1);  // ❌ Ошибка компиляции: значение перемещено
}

Ключевые слова определения типов

#![allow(unused)]
fn main() {
// struct — как dataclass или NamedTuple в Python
struct Point {               // @dataclass
    x: f64,                  // class Point:
    y: f64,                  //     x: float
}                            //     y: float

// enum — как enum в Python, но гораздо мощнее (хранит данные)
enum Shape {                 // Прямого аналога в Python нет
    Circle(f64),             // Каждый вариант может хранить свои данные
    Rectangle(f64, f64),
}

// impl — присоединяет методы к типу (как определение методов в классе)
impl Point {                 // class Point:
    fn distance(&self) -> f64 {  //     def distance(self) -> float:
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// trait — как ABC или Protocol (PEP 544) в Python
trait Drawable {             // class Drawable(Protocol):
    fn draw(&self);          //     def draw(self) -> None: ...
}

// type — псевдоним типа (как TypeAlias в Python)
type UserId = i64;           // UserId = int  (или TypeAlias)
}

Ключевые слова управления потоком

#![allow(unused)]
fn main() {
// match — исчерпывающее сопоставление с образцом (как match в Python 3.10+, но с обязательной проверкой)
match value {
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    _ => println!("other"),          // _ = подстановка (как case _: в Python)
}

// if let — деструктуризация с условием (по-питоновски: if (m := regex.match(s)):)
if let Some(x) = optional_value {
    println!("{}", x);
}

// loop — бесконечный цикл (как while True:)
loop {
    break;  // Для выхода нужен break
}

// for — итерация (как for в Python, но .iter() нужен чаще)
for item in collection.iter() {      // for item in collection:
    println!("{}", item);
}

// while let — цикл с деструктуризацией
while let Some(item) = stack.pop() {
    process(item);
}
}

Ключевые слова видимости

#![allow(unused)]
fn main() {
// pub — публичный (в Python нет настоящего private; используется соглашение с _)
pub fn greet() { }           // def greet():  — в Python всё «публично»

// pub(crate) — видим только внутри крейта
pub(crate) fn internal() { } // def _internal():  — соглашение с одним подчёркиванием

// (без ключевого слова) — приватный для модуля
fn private_helper() { }      // def __private():  — искажение имён с двумя подчёркиваниями

// В Python «приватность» — это джентльменское соглашение.
// В Rust приватность гарантирует компилятор.
}

Упражнения

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

Задание: создайте новый проект на Rust и напишите программу, которая:

  1. Объявляет переменную name с вашим именем (тип &str)
  2. Объявляет изменяемую переменную count, начальное значение 0
  3. Использует цикл for от 1..=5, чтобы увеличивать count и выводить "Привет, {name}! (счётчик: {count})"
  4. После цикла выводит, чётное или нечётное значение count, с помощью выражения match
🔑 Решение
cargo new hello_rust && cd hello_rust
// src/main.rs
fn main() {
    let name = "Pythonista";
    let mut count = 0u32;

    for _ in 1..=5 {
        count += 1;
        println!("Привет, {name}! (счётчик: {count})");
    }

    let parity = match count % 2 {
        0 => "чётное",
        _ => "нечётное",
    };
    println!("Итоговый счётчик {count} — {parity}");
}

Главные выводы:

  • let по умолчанию неизменяем (чтобы менять count, нужен mut)
  • 1..=5 — диапазон с включённой правой границей (аналог range(1, 6) в Python)
  • match — это выражение, которое возвращает значение
  • Никакого self и никакого if __name__ == "__main__", только fn main()

3. Встроенные типы и переменные

Переменные и изменяемость

Что вы узнаете: переменные, неизменяемые по умолчанию, явный mut, примитивные числовые типы в сравнении с int произвольной точности в Python, String и &str (самая трудная концепция на раннем этапе), форматирование строк и обязательные аннотации типов в Rust.

Сложность: 🟢 Начальный

Объявление переменных в Python

# Python — всё изменяемо, типизация динамическая
count = 0          # Изменяемая, тип выводится как int
count = 5          # ✅ Работает
count = "hello"    # ✅ Работает — тип может меняться! (динамическая типизация)

# «Константы» — это только соглашение:
MAX_SIZE = 1024    # Ничто не мешает потом написать MAX_SIZE = 999

Объявление переменных в Rust

#![allow(unused)]
fn main() {
// Rust — по умолчанию неизменяемые, статически типизированные
let count = 0;           // Неизменяемая, тип выводится как i32
// count = 5;            // ❌ Ошибка компиляции: нельзя дважды присвоить значение неизменяемой переменной
// count = "hello";      // ❌ Ошибка компиляции: ожидалось целое число, найден &str

let mut count = 0;       // Явно изменяемая
count = 5;               // ✅ Работает
// count = "hello";      // ❌ Тип по-прежнему нельзя менять

const MAX_SIZE: usize = 1024; // Настоящая константа — гарантируется компилятором
}

Ключевой сдвиг мышления для разработчиков на Python

#![allow(unused)]
fn main() {
// Python: переменные — это метки, которые указывают на объекты
// Rust: переменные — это именованные места хранения, которые ВЛАДЕЮТ своими значениями

// Затенение (shadowing) — характерно для Rust, очень полезно
let input = "42";              // &str
let input = input.parse::<i32>().unwrap();  // Теперь i32 — новая переменная с тем же именем
let input = input * 2;         // Теперь 84 — ещё одна новая переменная

// В Python вы бы просто переприсвоили значение и потеряли старый тип:
input = "42"
input = int(input)   # То же имя, другой тип — Python тоже это позволяет
Но в Rust каждый `let` создаёт действительно новую привязку. Старая исчезает.
}

Практический пример: счётчик

# Версия на Python
class Counter:
    def __init__(self):
        self.value = 0
    
    def increment(self):
        self.value += 1
    
    def get_value(self):
        return self.value

c = Counter()
c.increment()
print(c.get_value())  # 1
// Версия на Rust
struct Counter {
    value: i64,
}

impl Counter {
    fn new() -> Self {
        Counter { value: 0 }
    }

    fn increment(&mut self) {     // &mut self — этот метод будет изменять объект
        self.value += 1;
    }

    fn get_value(&self) -> i64 {  // &self — этот метод только читает
        self.value
    }
}

fn main() {
    let mut c = Counter::new();   // Нужен `mut`, чтобы вызвать increment()
    c.increment();
    println!("{}", c.get_value()); // 1
}

Ключевое отличие: в Rust &mut self в сигнатуре метода сообщает и вам, и компилятору, что increment изменяет счётчик. В Python любой метод может изменить что угодно, и чтобы это понять, нужно читать код.


Сравнение примитивных типов

flowchart LR
    subgraph Python ["Типы Python"]
        PI["int<br/>(произвольная точность)"]
        PF["float<br/>(только 64 бита)"]
        PB["bool"]
        PS["str<br/>(Unicode)"]
    end
    subgraph Rust ["Типы Rust"]
        RI["i8 / i16 / i32 / i64 / i128<br/>u8 / u16 / u32 / u64 / u128"]
        RF["f32 / f64"]
        RB["bool"]
        RS["String / &str"]
    end
    PI -->|"фиксированный размер"| RI
    PF -->|"выбор точности"| RF
    PB -->|"то же"| RB
    PS -->|"владение или заимствование"| RS
    style Python fill:#ffeeba
    style Rust fill:#d4edda

Числовые типы

PythonRustПримечания
int (произвольная точность)i8, i16, i32, i64, i128, isizeЦелые числа в Rust имеют фиксированный размер
int (беззнаковые: отдельного типа нет)u8, u16, u32, u64, u128, usizeБеззнаковые типы указываются явно
float (64-бит, IEEE 754)f32, f64В Python есть только 64-битный float
boolboolТа же концепция
complexНет встроенного (используйте крейт num)Редко встречается в системном коде
# Python — один целочисленный тип, произвольная точность
x = 42                     # int — может расти до любого размера
big = 2 ** 1000            # По-прежнему работает — тысячи цифр
y = 3.14                   # float — всегда 64-битный
#![allow(unused)]
fn main() {
// Rust — явные размеры, переполнение — ошибка компиляции или выполнения
let x: i32 = 42;           // 32-битное знаковое целое
let y: f64 = 3.14;         // 64-битное число с плавающей точкой (аналог float в Python)
let big: i128 = 2_i128.pow(100); // Максимум 128 бит — произвольной точности нет
// Для произвольной точности используйте крейт `num-bigint`

// Подчёркивания для читаемости (как 1_000_000 в Python):
let million = 1_000_000;   // Тот же синтаксис, что и в Python!

// Синтаксис суффиксов типа:
let a = 42u8;              // u8
let b = 3.14f32;           // f32
}

Типы размера (важно!)

#![allow(unused)]
fn main() {
// usize и isize — целые размером с указатель, используются для индексации
let length: usize = vec![1, 2, 3].len();  // .len() возвращает usize
let index: usize = 0;                     // Индексы массивов всегда usize

// В Python len() возвращает int, и индексы тоже int — различий нет.
// В Rust смешивание i32 и usize требует явного преобразования:
let i: i32 = 5;
// let item = vec[i];    // ❌ Ошибка: ожидался usize, найден i32
let item = vec[i as usize]; // ✅ Явное преобразование
}

Вывод типов

#![allow(unused)]
fn main() {
// Rust выводит типы, но они ФИКСИРОВАННЫЕ — не динамические
let x = 42;          // Компилятор выводит i32 (целый тип по умолчанию)
let y = 3.14;        // Компилятор выводит f64 (вещественный тип по умолчанию)
let s = "hello";     // Компилятор выводит &str (срез строки)
let v = vec![1, 2];  // Компилятор выводит Vec<i32>

// Можно всегда указать тип явно:
let x: i64 = 42;
let y: f32 = 3.14;

// В отличие от Python, тип НИКОГДА не меняется после вывода:
let x = 42;
// x = "hello";      // ❌ Ошибка: ожидалось целое число, найден &str
}

Строковые типы: String и &str

Это один из самых больших сюрпризов для разработчиков на Python. В Rust есть два основных строковых типа там, где в Python один.

Работа со строками в Python

# Python — один строковый тип, неизменяемый, с подсчётом ссылок
name = "Alice"          # str — неизменяемый, размещается в куче
greeting = f"Hello, {name}!"  # Форматирование через f-строку
chars = list(name)      # Преобразование в список символов
upper = name.upper()    # Возвращает новую строку (строки неизменяемы)

Строковые типы Rust

#![allow(unused)]
fn main() {
// В Rust ДВА строковых типа:

// 1. &str (срез строки) — заимствованный, неизменяемый, «вид» на данные строки
let name: &str = "Alice";           // Указывает на данные строки в бинарнике
                                     // Ближе всего к str в Python, но это ССЫЛКА

// 2. String (строка-владелец) — в куче, растёт, принадлежит владельцу
let mut greeting = String::from("Hello, ");  // Владеющая строка, её можно изменять
greeting.push_str(name);
greeting.push('!');
// greeting теперь "Hello, Alice!"
}

Когда что использовать?

#![allow(unused)]
fn main() {
// Думайте так:
// &str   = «Я смотрю на строку, которой владеет кто-то другой» (вид только для чтения)
// String = «Я владею этой строкой и могу её менять» (владеющие данные)

// Параметры функций: лучше &str (принимает оба типа)
fn greet(name: &str) -> String {          // принимает и &str, и &String
    format!("Hello, {}!", name)           // format! создаёт новый String
}

let s1 = "world";                         // литерал &str
let s2 = String::from("Rust");            // String

greet(s1);      // ✅ &str работает напрямую
greet(&s2);     // ✅ &String автоматически преобразуется в &str (Deref coercion)
}

Практические примеры

# Строковые операции в Python
name = "alice"
upper = name.upper()               # "ALICE"
contains = "lic" in name           # True
parts = "a,b,c".split(",")         # ["a", "b", "c"]
joined = "-".join(["a", "b", "c"]) # "a-b-c"
stripped = "  hello  ".strip()     # "hello"
replaced = name.replace("a", "A") # "Alice"
#![allow(unused)]
fn main() {
// Аналоги в Rust
let name = "alice";
let upper = name.to_uppercase();           // String — новое выделение памяти
let contains = name.contains("lic");       // bool
let parts: Vec<&str> = "a,b,c".split(',').collect();  // Vec<&str>
let joined = ["a", "b", "c"].join("-");    // String
let stripped = "  hello  ".trim();         // &str — без выделения памяти!
let replaced = name.replace("a", "A");     // String

// Ключевая мысль: одни операции возвращают &str (без выделения памяти), другие — String.
// .trim() возвращает срез исходной строки — это эффективно!
// .to_uppercase() должен создать новый String — нужно выделение памяти.
}

Разработчикам на Python: думайте так

Python str     ≈ Rust &str     (вы в основном читаете строки)
Python str     ≈ Rust String   (когда нужно владеть или изменять)

Правило большого пальца:
- Параметры функций        → используйте &str (самый гибкий вариант)
- Поля структур            → используйте String (структура владеет своими данными)
- Возвращаемые значения    → используйте String (вызывающий код должен владеть ими)
- Строковые литералы       → автоматически &str

Вывод и форматирование строк

Базовый вывод

# Python
print("Hello, World!")
print("Имя:", name, "Возраст:", age)    # Через пробел
print(f"Имя: {name}, Возраст: {age}")   # f-строка
#![allow(unused)]
fn main() {
// Rust
println!("Hello, World!");
println!("Имя: {} Возраст: {}", name, age);    // Позиционные {}
println!("Имя: {name}, Возраст: {age}");       // Встроенные переменные (Rust 1.58+, как f-строки!)
}

Спецификаторы формата

# Форматирование в Python
print(f"{3.14159:.2f}")          # "3.14" — 2 знака после запятой
print(f"{42:05d}")               # "00042" — дополнение нулями
print(f"{255:#x}")               # "0xff" — шестнадцатеричный вид
print(f"{42:>10}")               # "        42" — выравнивание по правому краю
print(f"{'left':<10}|")          # "left      |" — выравнивание по левому краю
#![allow(unused)]
fn main() {
// Форматирование в Rust (очень похоже на Python!)
println!("{:.2}", 3.14159);         // "3.14" — 2 знака после запятой
println!("{:05}", 42);              // "00042" — дополнение нулями
println!("{:#x}", 255);             // "0xff" — шестнадцатеричный вид
println!("{:>10}", 42);             // "        42" — выравнивание по правому краю
println!("{:<10}|", "left");        // "left      |" — выравнивание по левому краю
}

Отладочный вывод

# Python — repr() и pprint
print(repr([1, 2, 3]))             # "[1, 2, 3]"
from pprint import pprint
pprint({"key": [1, 2, 3]})         # Красивый вывод
#![allow(unused)]
fn main() {
// Rust — {:?} и {:#?}
println!("{:?}", vec![1, 2, 3]);       // "[1, 2, 3]" — формат Debug
println!("{:#?}", vec![1, 2, 3]);      // Красивый формат Debug

// Чтобы типы можно было вывести, добавьте derive Debug:
#[derive(Debug)]
struct Point { x: f64, y: f64 }

let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p);                   // "Point { x: 1.0, y: 2.0 }"
println!("{p:?}");                     // То же самое, но со встроенным синтаксисом
}

Краткая справка

PythonRustПримечания
print(x)println!("{}", x) или println!("{x}")Формат Display
print(repr(x))println!("{:?}", x)Формат Debug
f"Hello {name}"format!("Hello {name}")Возвращает String
print(x, end="")print!("{x}")Без перевода строки (print! и println!)
print(x, file=sys.stderr)eprintln!("{x}")Вывод в stderr
sys.stdout.write(s)print!("{s}")Без перевода строки

Аннотации типов: необязательные и обязательные

Подсказки типов в Python (необязательные, не проверяются)

# Python — подсказки типов служат документацией, а не принудительной проверкой
def add(a: int, b: int) -> int:
    return a + b

add(1, 2)         # ✅
add("a", "b")     # ✅ Python не возражает — возвращает "ab"
add(1, "2")       # ✅ Пока не упадёт во время выполнения: TypeError

# Union-типы, Optional
def find(key: str) -> int | None:
    ...

# Обобщённые типы
def first(items: list[int]) -> int | None:
    return items[0] if items else None

# Псевдонимы типов
UserId = int
Mapping = dict[str, list[int]]

Объявления типов в Rust (обязательные, проверяются компилятором)

#![allow(unused)]
fn main() {
// Rust — типы проверяются всегда, без исключений.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

add(1, 2);         // ✅
// add("a", "b");  // ❌ Ошибка компиляции: ожидался i32, найден &str

// Необязательные значения используют Option<T>
fn find(key: &str) -> Option<i32> {
    // Возвращает Some(value) или None
    Some(42)
}

// Обобщённые типы
fn first(items: &[i32]) -> Option<i32> {
    items.first().copied()
}

// Псевдонимы типов
type UserId = i64;
type Mapping = HashMap<String, Vec<i32>>;
}

Ключевая мысль: в Python подсказки типов помогают вашей IDE и mypy, но на выполнение программы не влияют. В Rust типы — это и есть программа: компилятор использует их, чтобы гарантировать безопасность памяти, предотвратить гонки данных и исключить ошибки нулевых указателей.

📌 См. также: Гл. 6 — Перечисления и сопоставление с образцом показывает, как система типов Rust заменяет Union-типы Python и проверки isinstance().


Упражнения

🏋️ Упражнение: конвертер температуры (нажмите, чтобы раскрыть)

Задание: напишите функцию celsius_to_fahrenheit(c: f64) -> f64 и функцию classify(temp_f: f64) -> &'static str, которая по пороговым значениям возвращает "холодно", "умеренно" или "жарко". Выведите результат для 0, 20 и 35 градусов Цельсия. Используйте форматирование строк.

🔑 Решение
fn celsius_to_fahrenheit(c: f64) -> f64 {
    c * 9.0 / 5.0 + 32.0
}

fn classify(temp_f: f64) -> &'static str {
    if temp_f < 50.0 { "холодно" }
    else if temp_f < 77.0 { "умеренно" }
    else { "жарко" }
}

fn main() {
    for c in [0.0, 20.0, 35.0] {
        let f = celsius_to_fahrenheit(c);
        println!("{c:.1}°C = {f:.1}°F — {}", classify(f));
    }
}

Ключевой вывод: Rust требует явного f64 (нет неявного преобразования int → float), for перебирает массивы напрямую (без range()), а блоки if/else — это выражения.


4. Управление потоком выполнения

Условные конструкции

Что вы узнаете: if/else без скобок (но с фигурными скобками), loop/while/for в сравнении с моделью итерации Python, блоки-выражения (всё возвращает значение) и сигнатуры функций с обязательными типами возвращаемых значений.

Сложность: 🟢 Начальный

if/else

# Python
if temperature > 100:
    print("Слишком жарко!")
elif temperature < 0:
    print("Слишком холодно!")
else:
    print("В самый раз")

# Тернарный оператор
status = "hot" if temperature > 100 else "ok"
#![allow(unused)]
fn main() {
// Rust — фигурные скобки обязательны, двоеточий нет, пишется `else if`, а не `elif`
if temperature > 100 {
    println!("Слишком жарко!");
} else if temperature < 0 {
    println!("Слишком холодно!");
} else {
    println!("В самый раз");
}

// if — это ВЫРАЖЕНИЕ, оно возвращает значение (как тернарный оператор Python, но мощнее)
let status = if temperature > 100 { "hot" } else { "ok" };
}

Важные различия

#![allow(unused)]
fn main() {
// 1. Условие должно быть bool — никакой «истинности» и «ложности»
let x = 42;
// if x { }          // ❌ Ошибка: ожидался bool, найден целый тип
if x != 0 { }        // ✅ Нужно явное сравнение

// В Python всё это приводится к True/False:
// if []:      → False    (пустой список)
// if "":      → False    (пустая строка)
// if 0:       → False    (ноль)
// if None:    → False

// В Rust в условиях работает ТОЛЬКО bool:
let items: Vec<i32> = vec![];
// if items { }           // ❌ Ошибка
if !items.is_empty() { }  // ✅ Явная проверка

let name = "";
// if name { }             // ❌ Ошибка
if !name.is_empty() { }    // ✅ Явная проверка
}

Циклы и итерация

Циклы for

# Python
for i in range(5):
    print(i)

for item in ["a", "b", "c"]:
    print(item)

for i, item in enumerate(["a", "b", "c"]):
    print(f"{i}: {item}")

for key, value in {"x": 1, "y": 2}.items():
    print(f"{key} = {value}")
#![allow(unused)]
fn main() {
// Rust
for i in 0..5 {                           // range(5) → 0..5
    println!("{}", i);
}

for item in ["a", "b", "c"] {             // Прямая итерация
    println!("{}", item);
}

for (i, item) in ["a", "b", "c"].iter().enumerate() {  // enumerate()
    println!("{}: {}", i, item);
}

// Итерация по HashMap
use std::collections::HashMap;
let map = HashMap::from([("x", 1), ("y", 2)]);
for (key, value) in &map {                // & — заимствование словаря
    println!("{} = {}", key, value);
}
}

Синтаксис диапазонов

#![allow(unused)]
fn main() {
Python:              Rust:               Примечания:
range(5)             0..5                Полуоткрытый (правая граница не включается)
range(1, 10)         1..10               Полуоткрытый
range(1, 11)         1..=10              Закрытый (правая граница включается)
range(0, 10, 2)      (0..10).step_by(2)  Шаг (метод, а не синтаксис)
}

Циклы while

# Python
count = 0
while count < 5:
    print(count)
    count += 1

# Бесконечный цикл
while True:
    data = get_input()
    if data == "quit":
        break
#![allow(unused)]
fn main() {
// Rust
let mut count = 0;
while count < 5 {
    println!("{}", count);
    count += 1;
}

// Бесконечный цикл — используйте `loop`, а не `while true`
loop {
    let data = get_input();
    if data == "quit" {
        break;
    }
}

// loop может вернуть значение! (уникально для Rust)
let result = loop {
    let input = get_input();
    if let Ok(num) = input.parse::<i32>() {
        break num;  // `break` со значением — как return для циклов
    }
    println!("Не число, попробуйте ещё раз");
};
}

Списочные включения и цепочки итераторов

# Python — списочные включения (list comprehensions)
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(x, y) for x in range(3) for y in range(3)]
#![allow(unused)]
fn main() {
// Rust — цепочки итераторов (.map, .filter, .collect)
let squares: Vec<i32> = (0..10).map(|x| x * x).collect();
let evens: Vec<i32> = (0..20).filter(|x| x % 2 == 0).collect();
let pairs: Vec<(i32, i32)> = (0..3)
    .flat_map(|x| (0..3).map(move |y| (x, y)))
    .collect();

// Они ЛЕНИВЫЕ — ничего не выполняется до .collect()
// Списочные включения в Python энергичные (выполняются сразу)
// Итераторы Rust могут быть эффективнее на больших наборах данных
}

Блоки-выражения

Всё в Rust является выражением (или может им быть). Это большой сдвиг по сравнению с Python, где if и for — это инструкции.

# Python — if — это инструкция (кроме тернарного оператора)
if condition:
    result = "yes"
else:
    result = "no"

# Или тернарный оператор (только одно выражение):
result = "yes" if condition else "no"
#![allow(unused)]
fn main() {
// Rust — if — это выражение (возвращает значение)
let result = if condition { "yes" } else { "no" };

// Блоки — это выражения: последняя строка (без точки с запятой) и есть возвращаемое значение
let value = {
    let x = 5;
    let y = 10;
    x + y    // Без точки с запятой → это значение блока (15)
};

// match — тоже выражение
let description = match temperature {
    t if t > 100 => "boiling",
    t if t > 50 => "hot",
    t if t > 20 => "warm",
    _ => "cold",
};
}

Диаграмма ниже иллюстрирует ключевое различие между управлением потоком в Python на основе инструкций и в Rust на основе выражений:

flowchart LR
    subgraph Python ["Python — инструкции"]
        P1["if condition:"] --> P2["result = 'yes'"]
        P1 --> P3["result = 'no'"]
        P2 --> P4["результат используется позже"]
        P3 --> P4
    end

    Python ~~~ Rust
    subgraph Rust ["Rust — выражения"]
        R1["let result = if cond"] --> R2["{ 'yes' }"]
        R1 --> R3["{ 'no' }"]
        R2 --> R4["значение возвращается напрямую"]
        R3 --> R4
    end
    style Python fill:#ffeeba
    style Rust fill:#d4edda

Правило точки с запятой: в Rust последнее выражение блока без точки с запятой — это возвращаемое значение блока. Добавленная точка с запятой превращает его в инструкцию (возвращается ()). Поначалу это сбивает с толку разработчиков на Python: это похоже на неявный return.


Функции и сигнатуры типов

Функции в Python

# Python — типы необязательны, диспетчеризация динамическая
def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

# Аргументы по умолчанию, *args, **kwargs
def flexible(*args, **kwargs):
    pass

# Функции первого класса
def apply(f, x):
    return f(x)

result = apply(lambda x: x * 2, 5)  # 10

Функции в Rust

#![allow(unused)]
fn main() {
// Rust — типы в сигнатурах функций ОБЯЗАТЕЛЬНЫ, значений по умолчанию нет
fn greet(name: &str, greeting: &str) -> String {
    format!("{}, {}!", greeting, name)
}

// Аргументов по умолчанию нет — используйте паттерн «строитель» или Option
fn greet_with_default(name: &str, greeting: Option<&str>) -> String {
    let greeting = greeting.unwrap_or("Hello");
    format!("{}, {}!", greeting, name)
}

// Нет *args/**kwargs — используйте срезы или структуры
fn sum_all(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

// Функции первого класса и замыкания
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

let result = apply(|x| x * 2, 5);  // 10
}

Возвращаемые значения

# Python — return пишется явно, None подразумевается
def divide(a, b):
    if b == 0:
        return None  # Или выбросить исключение
    return a / b
#![allow(unused)]
fn main() {
// Rust — последнее выражение и есть возвращаемое значение (без точки с запятой)
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None              // Ранний возврат (можно также написать `return None;`)
    } else {
        Some(a / b)       // Последнее выражение — неявный возврат
    }
}
}

Несколько возвращаемых значений

# Python — возвращаем кортеж
def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5])
#![allow(unused)]
fn main() {
// Rust — возвращаем кортеж (та же концепция!)
fn min_max(numbers: &[i32]) -> (i32, i32) {
    let min = *numbers.iter().min().unwrap();
    let max = *numbers.iter().max().unwrap();
    (min, max)
}

let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);
}

Методы: self, &self и &mut self

#![allow(unused)]
fn main() {
// В Python `self` — это всегда изменяемая ссылка на объект.
// В Rust выбираете вы:

impl MyStruct {
    fn new() -> Self { ... }                // Без self — «статический метод» / «classmethod»
    fn read_only(&self) { ... }             // &self — заимствует неизменяемо (нельзя менять)
    fn modify(&mut self) { ... }            // &mut self — заимствует изменяемо (можно менять)
    fn consume(self) { ... }                // self — забирает владение (объект перемещается)
}

// Аналог в Python:
// class MyStruct:
//     @classmethod
//     def new(cls): ...                    # Экземпляр не нужен
//     def read_only(self): ...             # В Python все три варианта одинаковы:
//     def modify(self): ...                # Python self всегда изменяем
//     def consume(self): ...               # Python никогда не «потребляет» self
}

Упражнения

🏋️ Упражнение: FizzBuzz на выражениях (нажмите, чтобы раскрыть)

Задание: напишите FizzBuzz для 1..=30 с использованием match как выражения Rust. Каждое число должно выводить "Fizz", "Buzz", "FizzBuzz" или само число. Используйте match (n % 3, n % 5) в качестве выражения.

🔑 Решение
fn main() {
    for n in 1..=30 {
        let result = match (n % 3, n % 5) {
            (0, 0) => String::from("FizzBuzz"),
            (0, _) => String::from("Fizz"),
            (_, 0) => String::from("Buzz"),
            _ => n.to_string(),
        };
        println!("{result}");
    }
}

Ключевой вывод: match — это выражение, которое возвращает значение, поэтому цепочки if/elif/else не нужны. Подстановка _ заменяет case _: из Python в роли значения по умолчанию.


5. Структуры данных и коллекции

Кортежи и деструктуризация

Что вы узнаете: кортежи Rust и Python, массивы и срезы, структуры (замена классам в Rust), Vec<T> и list, HashMap<K,V> и dict, а также паттерн newtype для моделирования предметной области.

Сложность: 🟢 Начальный

Кортежи в Python

# Python — кортежи это неизменяемые последовательности
point = (3.0, 4.0)
x, y = point                    # Распаковка
print(f"x={x}, y={y}")

# Кортежи могут содержать значения разных типов
record = ("Alice", 30, True)
name, age, active = record

# Именованные кортежи для ясности
from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(3.0, 4.0)
print(p.x)                      # Доступ по имени

Кортежи в Rust

#![allow(unused)]
fn main() {
// Rust — кортежи фиксированного размера, с типами, могут содержать значения разных типов
let point: (f64, f64) = (3.0, 4.0);
let (x, y) = point;              // Деструктуризация (то же, что распаковка в Python)
println!("x={x}, y={y}");

// Значения разных типов
let record: (&str, i32, bool) = ("Alice", 30, true);
let (name, age, active) = record;

// Доступ по индексу (в отличие от Python, используется синтаксис .0 .1 .2)
let first = record.0;            // "Alice"
let second = record.1;           // 30

// Python: record[0]
// Rust:   record.0      ← индекс через точку, а не квадратные скобки
}

Когда использовать кортежи, а когда структуры

#![allow(unused)]
fn main() {
// Кортежи: быстрая группировка, возврат из функций, временные значения
fn min_max(data: &[i32]) -> (i32, i32) {
    (*data.iter().min().unwrap(), *data.iter().max().unwrap())
}
let (lo, hi) = min_max(&[3, 1, 4, 1, 5]);

// Структуры: именованные поля, понятный смысл, методы
struct Point { x: f64, y: f64 }

// Эмпирическое правило:
// - 2–3 поля одного типа → кортеж подойдёт
// - Нужны именованные поля → используйте структуру
// - Нужны методы → используйте структуру
// (Те же рекомендации, что и в Python: tuple vs namedtuple vs dataclass)
}

Массивы и срезы

Списки Python и массивы Rust

# Python — списки динамические, гетерогенные
numbers = [1, 2, 3, 4, 5]       # Могут расти, уменьшаться, содержать значения разных типов
numbers.append(6)
mixed = [1, "two", 3.0]         # Смешанные типы допустимы
#![allow(unused)]
fn main() {
// В Rust есть ДВА понятия: фиксированный размер и динамический размер

// 1. Массив — фиксированный размер, размещается на стеке (аналога в Python нет)
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Размер входит в тип!
// numbers.push(6);  // ❌ Массивы не могут расти

// Инициализация всех элементов одним значением:
let zeros = [0; 10];            // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// 2. Срез — представление участка массива или Vec (как срезы в Python, но заимствованный)
let slice: &[i32] = &numbers[1..4]; // [2, 3, 4] — ссылка, а не копия!

// Python: numbers[1:4] создаёт НОВЫЙ список (копию)
// Rust:   &numbers[1..4] создаёт ПРЕДСТАВЛЕНИЕ (без копирования и выделения памяти)
}

Практическое сравнение

# Срезы в Python создают копии
data = [10, 20, 30, 40, 50]
first_three = data[:3]          # Новый список: [10, 20, 30]
last_two = data[-2:]            # Новый список: [40, 50]
reversed_data = data[::-1]      # Новый список: [50, 40, 30, 20, 10]
#![allow(unused)]
fn main() {
// Срезы в Rust создают представления (ссылки)
let data = [10, 20, 30, 40, 50];
let first_three = &data[..3];         // &[i32], представление: [10, 20, 30]
let last_two = &data[3..];            // &[i32], представление: [40, 50]

// Отрицательных индексов нет — используйте .len()
let last_two = &data[data.len()-2..]; // &[i32], представление: [40, 50]

// Разворот: используйте итератор
let reversed: Vec<i32> = data.iter().rev().copied().collect();
}

Структуры и классы

Классы в Python

# Python — класс с __init__, методами и свойствами
from dataclasses import dataclass

@dataclass
class Rectangle:
    width: float
    height: float

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2.0 * (self.width + self.height)

    def scale(self, factor: float) -> "Rectangle":
        return Rectangle(self.width * factor, self.height * factor)

    def __str__(self) -> str:
        return f"Rectangle({self.width} x {self.height})"

r = Rectangle(10.0, 5.0)
print(r.area())         # 50.0
print(r)                # Rectangle(10.0 x 5.0)

Структуры в Rust

// Rust — struct + блоки impl (без наследования!)
#[derive(Debug, Clone)]
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // «Конструктор» — ассоциированная функция (без self)
    fn new(width: f64, height: f64) -> Self {
        Rectangle { width, height }   // Сокращённая запись полей, когда имена совпадают
    }

    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn scale(&self, factor: f64) -> Rectangle {
        Rectangle::new(self.width * factor, self.height * factor)
    }
}

// Трейт Display — аналог __str__ в Python
impl std::fmt::Display for Rectangle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Rectangle({} x {})", self.width, self.height)
    }
}

fn main() {
    let r = Rectangle::new(10.0, 5.0);
    println!("{}", r.area());    // 50.0
    println!("{}", r);           // Rectangle(10 x 5)
}
flowchart LR
    subgraph Python ["Объект Python (куча)"]
        PH["Заголовок PyObject<br/>(refcount + указатель на тип)"] --> PW["width: объект float"]
        PH --> PHT["height: объект float"]
        PH --> PD["__dict__"]
    end
    Python ~~~ Rust
    subgraph Rust ["Структура Rust (стек)"]
        RW["width: f64<br/>(8 байт)"] --- RH["height: f64<br/>(8 байт)"]
    end
    style Python fill:#ffeeba
    style Rust fill:#d4edda

Наблюдение о памяти: объект Python Rectangle имеет 56-байтовый заголовок плюс отдельные объекты float в куче. Структура Rust Rectangle занимает ровно 16 байт на стеке: без косвенных обращений и без нагрузки на GC.

📌 См. также: Гл. 10 — Трейты и обобщения рассказывает о реализации трейтов вроде Display, Debug и перегрузки операторов для ваших структур.

Соответствие: магические методы Python → трейты Rust

PythonRustНазначение
__str__impl DisplayЧеловекочитаемая строка
__repr__#[derive(Debug)]Отладочное представление
__eq__#[derive(PartialEq)]Сравнение на равенство
__hash__#[derive(Hash)]Хешируемость (для ключей dict и HashSet)
__lt__, __le__ и т. д.#[derive(PartialOrd, Ord)]Упорядочивание
__add__impl AddОператор +
__iter__impl IteratorИтерация
__len__метод .len()Длина
__enter__/__exit__RAII + impl DropАвтоматическая очистка; прямого аналога двухфазного протокола менеджера контекста нет
__init__fn new() (соглашение)Конструктор
__getitem__impl IndexИндексация через []
__contains__метод .contains()Оператор in

Нет наследования: вместо него композиция

# Python — наследование
class Animal:
    def __init__(self, name: str):
        self.name = name
    def speak(self) -> str:
        raise NotImplementedError

class Dog(Animal):
    def speak(self) -> str:
        return f"{self.name} говорит: Гав!"

class Cat(Animal):
    def speak(self) -> str:
        return f"{self.name} говорит: Мяу!"
#![allow(unused)]
fn main() {
// Rust — трейты и композиция (без наследования)
trait Animal {
    fn name(&self) -> &str;
    fn speak(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn speak(&self) -> String {
        format!("{} говорит: Гав!", self.name)
    }
}

impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn speak(&self) -> String {
        format!("{} говорит: Мяу!", self.name)
    }
}

// Для полиморфизма используйте трейт-объекты (как утиную типизацию в Python):
fn animal_roll_call(animals: &[&dyn Animal]) {
    for a in animals {
        println!("{}", a.speak());
    }
}
}

Ментальная модель: Python говорит «унаследуй поведение». Rust говорит «реализуй контракт». Результат похож, но Rust избегает проблемы ромбовидного наследования и хрупкости базовых классов.


Vec и list

Vec<T> — это растущий массив Rust в куче, ближайший аналог list в Python.

Создание векторов

# Python
numbers = [1, 2, 3]
empty = []
repeated = [0] * 10
from_range = list(range(1, 6))
#![allow(unused)]
fn main() {
// Rust
let numbers = vec![1, 2, 3];            // макрос vec! (аналог литерала списка)
let empty: Vec<i32> = Vec::new();        // Пустой vec (нужна аннотация типа)
let repeated = vec![0; 10];              // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let from_range: Vec<i32> = (1..6).collect(); // [1, 2, 3, 4, 5]
}

Частые операции

# Операции со списками Python
nums = [1, 2, 3]
nums.append(4)                   # [1, 2, 3, 4]
nums.extend([5, 6])             # [1, 2, 3, 4, 5, 6]
nums.insert(0, 0)               # [0, 1, 2, 3, 4, 5, 6]
last = nums.pop()               # 6, nums = [0, 1, 2, 3, 4, 5]
length = len(nums)              # 6
nums.sort()                     # Сортировка на месте
sorted_copy = sorted(nums)     # Новый отсортированный список
nums.reverse()                  # Разворот на месте
contains = 3 in nums           # True
index = nums.index(3)          # Индекс первой тройки
#![allow(unused)]
fn main() {
// Операции с Vec в Rust
let mut nums = vec![1, 2, 3];
nums.push(4);                          // [1, 2, 3, 4]
nums.extend([5, 6]);                   // [1, 2, 3, 4, 5, 6]
nums.insert(0, 0);                     // [0, 1, 2, 3, 4, 5, 6]
let last = nums.pop();                 // Some(6), nums = [0, 1, 2, 3, 4, 5]
let length = nums.len();               // 6
nums.sort();                           // Сортировка на месте
let mut sorted_copy = nums.clone();
sorted_copy.sort();                    // Сортируем копию
nums.reverse();                        // Разворот на месте
let contains = nums.contains(&3);      // true
let index = nums.iter().position(|&x| x == 3); // Some(index) или None
}

Краткая справка

PythonRustПримечания
lst.append(x)vec.push(x)
lst.extend(other)vec.extend(other)
lst.pop()vec.pop()Возвращает Option<T>
lst.insert(i, x)vec.insert(i, x)
lst.remove(x)vec.iter().position(|v| v == &x).map(|i| vec.remove(i))Удаляет только первое совпадение (чтобы удалить все, используйте retain)
del lst[i]vec.remove(i)Возвращает удалённый элемент
len(lst)vec.len()
x in lstvec.contains(&x)
lst.sort()vec.sort()
sorted(lst)Клонирование + sort или итератор
lst[i]vec[i]Паника, если индекс вне границ
lst.get(i, default)vec.get(i)Возвращает Option<&T>
lst[1:3]&vec[1..3]Возвращает срез (без копирования)

HashMap и dict

HashMap<K, V> — хеш-таблица Rust, аналог dict в Python.

Создание HashMap

# Python
scores = {"Alice": 100, "Bob": 85}
empty = {}
from_pairs = dict([("x", 1), ("y", 2)])
comprehension = {k: v for k, v in zip(keys, values)}
#![allow(unused)]
fn main() {
// Rust
use std::collections::HashMap;

let scores = HashMap::from([("Alice", 100), ("Bob", 85)]);
let empty: HashMap<String, i32> = HashMap::new();
let from_pairs: HashMap<&str, i32> = [("x", 1), ("y", 2)].into_iter().collect();
let comprehension: HashMap<_, _> = keys.iter().zip(values.iter()).collect();
}

Частые операции

# Операции со словарём Python
d = {"a": 1, "b": 2}
d["c"] = 3                      # Вставка
val = d["a"]                     # 1 (KeyError, если ключа нет)
val = d.get("z", 0)             # 0 (значение по умолчанию, если ключа нет)
del d["b"]                       # Удаление
exists = "a" in d               # True
keys = list(d.keys())           # ["a", "c"]
values = list(d.values())       # [1, 3]
items = list(d.items())         # [("a", 1), ("c", 3)]
length = len(d)                 # 2

# setdefault / defaultdict
from collections import defaultdict
word_count = defaultdict(int)
for word in words:
    word_count[word] += 1
#![allow(unused)]
fn main() {
// Операции с HashMap в Rust
use std::collections::HashMap;

let mut d = HashMap::new();
d.insert("a", 1);
d.insert("b", 2);
d.insert("c", 3);                       // Вставка или перезапись

let val = d["a"];                        // 1 (паника, если ключа нет)
let val = d.get("z").copied().unwrap_or(0); // 0 (безопасный доступ)
d.remove("b");                          // Удаление
let exists = d.contains_key("a");       // true
let keys: Vec<_> = d.keys().collect();
let values: Vec<_> = d.values().collect();
let length = d.len();

// Entry API — аналог паттерна setdefault / defaultdict в Python
let mut word_count: HashMap<&str, i32> = HashMap::new();
for word in words {
    *word_count.entry(word).or_insert(0) += 1;
}
}

Краткая справка

PythonRustПримечания
d[key] = vald.insert(key, val)Возвращает Option<V> (старое значение)
d[key]d[&key]Паника, если ключа нет
d.get(key)d.get(&key)Возвращает Option<&V>
d.get(key, default)d.get(&key).unwrap_or(&default)
key in dd.contains_key(&key)
del d[key]d.remove(&key)Возвращает Option<V>
d.keys()d.keys()Итератор
d.values()d.values()Итератор
d.items()d.iter()Итератор по (&K, &V)
len(d)d.len()
d.update(other)d.extend(other)
defaultdict(int).entry().or_insert(0)Entry API
d.setdefault(k, v)d.entry(k).or_insert(v)Entry API

Другие коллекции

PythonRustПримечания
set()HashSet<T>use std::collections::HashSet;
collections.dequeVecDeque<T>use std::collections::VecDeque;
heapqBinaryHeap<T>Max-куча по умолчанию
collections.OrderedDictIndexMap (крейт)HashMap не сохраняет порядок
sortedcontainers.SortedListBTreeSet<T> / BTreeMap<K,V>На основе дерева, отсортировано

Упражнения

🏋️ Упражнение: счётчик частоты слов (нажмите, чтобы раскрыть)

Задание: напишите функцию, которая принимает предложение &str и возвращает HashMap<String, usize> с частотами слов (без учёта регистра). В Python это Counter(s.lower().split()). Перепишите её на Rust.

🔑 Решение
use std::collections::HashMap;

fn word_frequencies(text: &str) -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    for word in text.split_whitespace() {
        let key = word.to_lowercase();
        *counts.entry(key).or_insert(0) += 1;
    }
    counts
}

fn main() {
    let text = "the quick brown fox jumps over the lazy fox";
    let freq = word_frequencies(text);
    for (word, count) in &freq {
        println!("{word}: {count}");
    }
}

Ключевой вывод: HashMap::entry().or_insert() — аналог defaultdict или Counter в Python. Разыменование * нужно, потому что or_insert возвращает &mut usize.


6. Перечисления и сопоставление с образцом

Алгебраические типы данных и объединения типов

Что вы узнаете: перечисления Rust с данными в сравнении с типами Union в Python, исчерпывающий match в сравнении с match/case, Option<T> как замену None на этапе компиляции и защитные условия (guards).

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

Python 3.10 ввёл оператор match и объединения типов. Перечисления Rust идут дальше: каждый вариант может хранить собственные данные, а компилятор следит, чтобы обрабатывался каждый случай.

Объединения типов и match в Python

# Python 3.10+ — структурное сопоставление с образцом
from typing import Union
from dataclasses import dataclass

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

@dataclass
class Triangle:
    base: float
    height: float

Shape = Union[Circle, Rectangle, Triangle]  # Псевдоним типа

def area(shape: Shape) -> float:
    match shape:
        case Circle(radius=r):
            return 3.14159 * r * r
        case Rectangle(width=w, height=h):
            return w * h
        case Triangle(base=b, height=h):
            return 0.5 * b * h
        # Компилятор не предупредит, если вы что-то пропустите!
        # Добавляете новую фигуру? Остаётся надеяться, что вы нашли все блоки match через grep.

Перечисления Rust с вариантами, которые хранят данные

#![allow(unused)]
fn main() {
// Rust — варианты перечисления хранят данные, компилятор требует исчерпывающего сопоставления
enum Shape {
    Circle(f64),                // Круг хранит радиус
    Rectangle(f64, f64),        // Прямоугольник хранит ширину и высоту
    Triangle { base: f64, height: f64 }, // Можно использовать и именованные поля
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle { base, height } => 0.5 * base * height,
        // ❌ Если вы добавите Shape::Pentagon и забудете обработать его здесь,
        //    компилятор откажется собирать программу. grep не нужен.
    }
}
}

Ключевая мысль: match в Rust — исчерпывающий: компилятор проверяет, что вы обработали каждый вариант. Добавьте новый вариант в перечисление, и компилятор точно укажет, в каких блоках match его нужно учесть. У match в Python такой гарантии нет.

Перечисления заменяют несколько паттернов Python

# Python — несколько паттернов, которые заменяют перечисления Rust:

# 1. Строковые константы
STATUS_PENDING = "pending"
STATUS_ACTIVE = "active"
STATUS_CLOSED = "closed"

# 2. Перечисление Python (без данных)
from enum import Enum
class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    CLOSED = "closed"

# 3. Теговые объединения (класс + поле типа)
class Message:
    def __init__(self, kind, **data):
        self.kind = kind
        self.data = data
# Message(kind="text", content="hello")
# Message(kind="image", url="...", width=100)
#![allow(unused)]
fn main() {
// Rust — одно перечисление заменяет все три и даже больше

// 1. Простое перечисление (как Enum в Python)
enum Status {
    Pending,
    Active,
    Closed,
}

// 2. Перечисление с данными (теговое объединение, с проверкой типов!)
enum Message {
    Text(String),
    Image { url: String, width: u32, height: u32 },
    Quit,                    // Без данных
    Move { x: i32, y: i32 },
}
}
flowchart TD
    E["enum Message"] --> T["Text(String)<br/>🏷️ tag=0 + данные String"]
    E --> I["Image { url, width, height }<br/>🏷️ tag=1 + 3 поля"]
    E --> Q["Quit<br/>🏷️ tag=2 + без данных"]
    E --> M["Move { x, y }<br/>🏷️ tag=3 + 2 поля"]
    style E fill:#d4edda,stroke:#28a745
    style T fill:#fff3cd
    style I fill:#fff3cd
    style Q fill:#fff3cd
    style M fill:#fff3cd

Наблюдение о памяти: перечисления Rust — это «помеченные объединения» (tagged unions): компилятор хранит дискриминант-тег и место под самый большой вариант. У Python-аналога (Union[str, dict, None]) компактного представления нет.

📌 См. также: Гл. 9 — Обработка ошибок широко использует перечисления: Result<T, E> и Option<T> — это просто перечисления и match.

#![allow(unused)]
fn main() {
fn process(msg: &Message) {
    match msg {
        Message::Text(content) => println!("Текст: {content}"),
        Message::Image { url, width, height } => {
            println!("Изображение: {url} ({width}x{height})")
        }
        Message::Quit => println!("Выход"),
        Message::Move { x, y } => println!("Перемещение в ({x}, {y})"),
    }
}
}

Исчерпывающее сопоставление с образцом

match в Python — не исчерпывающий

# Python — случай-«заглушка» необязателен, помощи компилятора нет
def describe(value):
    match value:
        case 0:
            return "zero"
        case 1:
            return "one"
        # Если вы забудете default, Python молча вернёт None.
        # Ни предупреждения, ни ошибки.

describe(42)  # Возвращает None — молчаливая ошибка

match в Rust — проверяется компилятором

#![allow(unused)]
fn main() {
// Rust — ОБЯЗАТЕЛЬНО обработать каждый возможный случай
fn describe(value: i32) -> &'static str {
    match value {
        0 => "zero",
        1 => "one",
        // ❌ Ошибка компиляции: паттерны не исчерпывающие: `i32::MIN..=-1_i32`
        //    и `2_i32..=i32::MAX` не покрыты
        _ => "other",   // _ = «ловушка» для всех остальных значений (обязательна для типов с открытым множеством значений)
    }
}

// Для перечислений «ловушка» НЕ нужна — компилятор знает все варианты:
enum Color { Red, Green, Blue }

fn color_hex(c: Color) -> &'static str {
    match c {
        Color::Red => "#ff0000",
        Color::Green => "#00ff00",
        Color::Blue => "#0000ff",
        // _ не нужен — все варианты покрыты
        // Добавите Color::Yellow позже → ошибка компиляции ЗДЕСЬ
    }
}
}

Возможности сопоставления с образцом

#![allow(unused)]
fn main() {
// Несколько значений (как case 1 | 2 | 3: в Python)
match value {
    1 | 2 | 3 => println!("маленькое"),
    4..=9 => println!("среднее"),    // Диапазоны
    _ => println!("большое"),
}

// Защитные условия (guards, как case x if x > 0: в Python)
match temperature {
    t if t > 100 => println!("кипение"),
    t if t < 0 => println!("ниже нуля"),
    t => println!("норма: {t}°"),
}

// Вложенная деструктуризация
let point = (3, (4, 5));
match point {
    (0, _) => println!("на оси Y"),
    (_, (0, _)) => println!("y=0"),
    (x, (y, z)) => println!("x={x}, y={y}, z={z}"),
}
}

Option для безопасной работы с None

Option<T> — самое важное перечисление Rust для разработчиков на Python. Оно заменяет None безопасной по типам альтернативой.

None в Python

# Python — None — это значение, которое может появиться где угодно
def find_user(user_id: int) -> dict | None:
    users = {1: {"name": "Alice"}}
    return users.get(user_id)

user = find_user(999)
# user — None, но ничто не заставляет вас проверить это!
print(user["name"])  # 💥 TypeError во время выполнения

Option в Rust

#![allow(unused)]
fn main() {
// Rust — Option<T> заставляет обработать случай None
fn find_user(user_id: i64) -> Option<User> {
    let users = HashMap::from([(1, User { name: "Alice".into() })]);
    users.get(&user_id).cloned()
}

let user = find_user(999);
// user имеет тип Option<User> — НЕЛЬЗЯ использовать его, не обработав None

// Способ 1: match
match find_user(999) {
    Some(user) => println!("Найден: {}", user.name),
    None => println!("Не найден"),
}

// Способ 2: if let (как if (x := expr) is not None в Python)
if let Some(user) = find_user(1) {
    println!("Найден: {}", user.name);
}

// Способ 3: unwrap_or
let name = find_user(999)
    .map(|u| u.name)
    .unwrap_or_else(|| "Unknown".to_string());

// Способ 4: оператор ? (в функциях, которые возвращают Option)
fn get_user_name(id: i64) -> Option<String> {
    let user = find_user(id)?;     // Досрочно возвращает None, если пользователь не найден
    Some(user.name)
}
}

Методы Option: аналоги в Python

ЗадачаPythonRust
Проверка наличияif x is not None:if let Some(x) = opt {
Значение по умолчаниюx or defaultopt.unwrap_or(default)
Фабрика значения по умолчаниюx or compute()opt.unwrap_or_else(|| compute())
Преобразование, если значение естьf(x) if x else Noneopt.map(f)
Цепочка обращенийx and x.attr and x.attr.method()opt.and_then(|x| x.method())
Падение при NoneНельзя предотвратитьopt.unwrap() (паника) или opt.expect("msg")
Получить значение или выбросить ошибкуx if x else raiseopt.ok_or(Error)?

Упражнения

🏋️ Упражнение: калькулятор площади фигур (нажмите, чтобы раскрыть)

Задание: определите перечисление Shape с вариантами Circle(f64) (радиус), Rectangle(f64, f64) (ширина, высота) и Triangle(f64, f64) (основание, высота). Реализуйте метод fn area(&self) -> f64 с использованием match. Создайте по одной фигуре каждого вида и выведите их площади.

🔑 Решение
use std::f64::consts::PI;

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle(b, h) => 0.5 * b * h,
        }
    }
}

fn main() {
    let shapes = [
        Shape::Circle(5.0),
        Shape::Rectangle(4.0, 6.0),
        Shape::Triangle(3.0, 8.0),
    ];
    for shape in &shapes {
        println!("Площадь: {:.2}", shape.area());
    }
}

Ключевой вывод: перечисления Rust заменяют Union[Circle, Rectangle, Triangle] и проверки isinstance() в Python. Компилятор следит, чтобы обрабатывался каждый вариант: добавление новой фигуры без обновления area() — ошибка компиляции.


7. Владение и заимствование

Понимание владения

Что вы узнаете: зачем в Rust нужно владение (без GC!), семантику перемещения в сравнении с подсчётом ссылок в Python, заимствование (& и &mut), основы времён жизни и умные указатели (Box, Rc, Arc).

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

Это самая трудная концепция для разработчиков на Python. В Python вы никогда не задумываетесь о том, кто «владеет» данными: этим занимается сборщик мусора. В Rust у каждого значения ровно один владелец, и компилятор отслеживает это на этапе компиляции.

Python: повсюду разделяемые ссылки

# Python — всё является ссылкой, сборщик мусора всё очищает
a = [1, 2, 3]
b = a              # b и a указывают на ОДИН И ТОТ ЖЕ список
b.append(4)
print(a)            # [1, 2, 3, 4] — сюрприз! a тоже изменился

# Кто владеет списком? Обе переменные ссылаются на него.
# Сборщик мусора освободит его, когда не останется ссылок.
# Об этом вы никогда не задумываетесь.

Rust: единоличное владение

#![allow(unused)]
fn main() {
// Rust — у каждого значения ровно ОДИН владелец
let a = vec![1, 2, 3];
let b = a;           // Владение ПЕРЕМЕЩАЕТСЯ из a в b
// println!("{:?}", a); // ❌ Ошибка компиляции: значение использовано после перемещения

// a больше не существует. b — единственный владелец.
println!("{:?}", b); // ✅ [1, 2, 3]

// Когда b выходит из области видимости, Vec освобождается. Детерминированно. Без GC.
}

Три правила владения

#![allow(unused)]
fn main() {
1. У каждого значения ровно ОДНА переменная-владелец.
2. Когда владелец выходит из области видимости, значение уничтожается (освобождается).
3. Владение можно передать (переместить), но нельзя продублировать (если тип не реализует Clone).
}

Семантика перемещения: самый большой шок для Python

# Python — присваивание копирует ссылку, а не данные
def process(data):
    data.append(42)
    # Исходный список изменён!

my_list = [1, 2, 3]
process(my_list)
print(my_list)       # [1, 2, 3, 42] — изменён функцией process!
#![allow(unused)]
fn main() {
// Rust — передача в функцию ПЕРЕМЕЩАЕТ владение (для типов, не реализующих Copy)
fn process(mut data: Vec<i32>) -> Vec<i32> {
    data.push(42);
    data  // Нужно вернуть его, чтобы вернуть владение!
}

let my_vec = vec![1, 2, 3];
let my_vec = process(my_vec);  // Владение уходит внутрь и возвращается обратно
println!("{:?}", my_vec);      // [1, 2, 3, 42]

// Или лучше — заимствовать вместо перемещения:
fn process_borrowed(data: &mut Vec<i32>) {
    data.push(42);
}

let mut my_vec = vec![1, 2, 3];
process_borrowed(&mut my_vec);  // Временно одалживаем
println!("{:?}", my_vec);       // [1, 2, 3, 42] — по-прежнему наш
}

Владение в наглядном виде

Python:                              Rust:

  a ──────┐                           a ──→ [1, 2, 3]
           ├──→ [1, 2, 3]
  b ──────┘                           После: let b = a;

  (a и b делят один объект)           a  (недействительна, перемещена)
  (refcount = 2)                      b ──→ [1, 2, 3]
                                      (владеет данными только b)

  del a → refcount = 1                drop(b) → данные освобождены
  del b → refcount = 0 → освобождено  (детерминированно, без GC)
stateDiagram-v2
    state "Python (подсчёт ссылок)" as PY {
        [*] --> a_owns: a = [1,2,3]
        a_owns --> shared: b = a
        shared --> b_only: del a (refcount 2→1)
        b_only --> freed: del b (refcount 1→0)
        note right of shared: и a, и b указывают<br/>на ОДИН И ТОТ ЖЕ объект
    }
    state "Rust (перемещение владения)" as RS {
        [*] --> a_owns2: let a = vec![1,2,3]
        a_owns2 --> b_owns: let b = a (MOVE)
        b_owns --> freed2: b выходит из области видимости
        note right of b_owns: a НЕДЕЙСТВИТЕЛЬНА после перемещения<br/>При использовании — ошибка компиляции
    }

Семантика перемещения и подсчёт ссылок

Копирование и перемещение

#![allow(unused)]
fn main() {
// Простые типы (целые, числа с плавающей точкой, bool, char) КОПИРУЮТСЯ, а не перемещаются
let x = 42;
let y = x;    // x КОПИРУЕТСЯ в y (оба остаются действительными)
println!("{x} {y}");  // ✅ 42 42

// Типы, размещённые в куче (String, Vec, HashMap), ПЕРЕМЕЩАЮТСЯ
let s1 = String::from("hello");
let s2 = s1;  // s1 ПЕРЕМЕЩАЕТСЯ в s2
// println!("{s1}");  // ❌ Ошибка: значение использовано после перемещения

// Чтобы явно скопировать данные из кучи, используйте .clone()
let s1 = String::from("hello");
let s2 = s1.clone();  // Глубокая копия
println!("{s1} {s2}");  // ✅ hello hello (оба действительны)
}

Ментальная модель разработчика на Python

Python:                    Rust:
─────────                  ─────
int, float, bool           Копируемые типы (i32, f64, bool, char)
→ общие ссылки на          → побитовое копирование при присваивании
  неизменяемые объекты       (всегда независимые значения)
  (настоящей копии нет)    (Примечание: Python кэширует малые int; копии в Rust всегда предсказуемы)

list, dict, str            Типы с перемещением (Vec, HashMap, String)
→ общая ссылка             → передача владения (поведение другое!)
→ сборщик мусора очищает   → владелец уничтожает данные
→ копия через list(x)      → копия через x.clone()
   или copy.deepcopy(x)

Когда модель совместного использования в Python приводит к ошибкам

# Python — случайное создание псевдонимов
def remove_duplicates(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

original = [1, 2, 2, 3, 3, 3]
alias = original          # Псевдоним, НЕ копия
unique = remove_duplicates(alias)
# original по-прежнему [1, 2, 2, 3, 3, 3] — но только потому, что мы его не меняли
# Если бы remove_duplicates изменял входные данные, original тоже пострадал бы
#![allow(unused)]
fn main() {
use std::collections::HashSet;

// Rust — владение предотвращает случайное создание псевдонимов
fn remove_duplicates(items: &[i32]) -> Vec<i32> {
    let mut seen = HashSet::new();
    items.iter()
        .filter(|&&item| seen.insert(item))
        .copied()
        .collect()
}

let original = vec![1, 2, 2, 3, 3, 3];
let unique = remove_duplicates(&original); // Заимствование — изменить нельзя
// original гарантированно не изменится — компилятор запретил мутацию через &
}

Заимствование и времена жизни

Заимствование = одолжить книгу

#![allow(unused)]
fn main() {
Представьте владение как физическую книгу:

Python:  У каждого есть фотокопия (общие ссылки + GC)
Rust:    Книга принадлежит одному человеку. Остальные могут:
         - &book     = посмотреть (неизменяемое заимствование, таких может быть много)
         - &mut book = писать в ней (изменяемое заимствование, эксклюзивное)
         - book      = отдать её (перемещение)
}

Правила заимствования

flowchart TD
    R["Правила заимствования"] --> IMM["✅ Много &T<br/>(общие/неизменяемые)"]
    R --> MUT["✅ Один &mut T<br/>(эксклюзивный/изменяемый)"]
    R --> CONFLICT["❌ &T + &mut T<br/>(НИКОГДА одновременно)"]
    IMM --> SAFE["Много читателей, безопасно"]
    MUT --> SAFE2["Один писатель, безопасно"]
    CONFLICT --> ERR["Ошибка компиляции!"]
    style IMM fill:#d4edda
    style MUT fill:#d4edda
    style CONFLICT fill:#f8d7da
    style ERR fill:#f8d7da,stroke:#dc3545
#![allow(unused)]
fn main() {
// Правило 1: можно иметь МНОГО неизменяемых заимствований ИЛИ ОДНО изменяемое (но не оба сразу)

let mut data = vec![1, 2, 3];

// Несколько неизменяемых заимствований — допустимо
let a = &data;
let b = &data;
println!("{:?} {:?}", a, b);  // ✅

// Изменяемое заимствование — должно быть эксклюзивным
let c = &mut data;
c.push(4);
// println!("{:?}", a);  // ❌ Ошибка: нельзя использовать неизменяемое заимствование, пока существует изменяемое

// Так гонки данных предотвращаются на этапе компиляции!
// В Python аналога нет: именно поэтому изменение словаря во время итерации приводит к падению во время выполнения.
}

Времена жизни: краткое введение

#![allow(unused)]
fn main() {
// Времена жизни отвечают на вопрос: «Как долго живёт эта ссылка?»
// Обычно компилятор выводит их сам. Вы редко пишете их явно.

// Простой случай — компилятор справляется сам:
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}
// Компилятор знает: возвращаемый &str живёт столько же, сколько входной &str

// Когда нужны явные времена жизни (редко):
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
// 'a означает: «возвращаемое значение живёт, пока живут оба входных параметра»
}

Для разработчиков на Python: поначалу не беспокойтесь о временах жизни. Компилятор сам скажет, когда они понадобятся, а в 95% случаев он выводит их автоматически. Думайте об аннотациях времён жизни как о подсказках, которые вы даёте компилятору, когда он не может сам разобраться в связях.


Умные указатели

Там, где единоличного владения недостаточно, Rust предоставляет умные указатели. Они ближе к модели ссылок Python, но явные и включаются осознанно.

#![allow(unused)]
fn main() {
// Box<T> — выделение в куче с одним владельцем (как обычное выделение памяти в Python)
let boxed = Box::new(42);  // i32 в куче

// Rc<T> — подсчёт ссылок (как refcount в Python!)
use std::rc::Rc;
let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared);  // Увеличиваем счётчик ссылок
let clone2 = Rc::clone(&shared);  // Увеличиваем счётчик ссылок
// Все три указывают на один Vec. Когда все они уничтожены, Vec освобождается.
// Похоже на подсчёт ссылок в Python, но Rc НЕ обрабатывает циклы —
// используйте Weak<T>, чтобы разрывать циклы (сборщик мусора Python обрабатывает циклы автоматически)

// Arc<T> — атомарный подсчёт ссылок (Rc для многопоточного кода)
use std::sync::Arc;
let thread_safe = Arc::new(vec![1, 2, 3]);
// Используйте Arc, когда данные разделяются между потоками (Rc — только для одного потока)

// RefCell<T> — проверка заимствований во время выполнения (как модель «всё можно» в Python)
use std::cell::RefCell;
let cell = RefCell::new(42);
*cell.borrow_mut() = 99;  // Изменяемое заимствование во время выполнения (паника при двойном заимствовании)
}

Когда что использовать

Умный указательАналог в PythonСценарий использования
Box<T>Обычное выделение памятиБольшие данные, рекурсивные типы, трейт-объекты
Rc<T>Стандартный подсчёт ссылок PythonОбщее владение, один поток
Arc<T>Потокобезопасный подсчёт ссылокОбщее владение, несколько потоков
RefCell<T>Модель «просто меняй» в PythonВнутренняя изменяемость (запасной люк)
Rc<RefCell<T>>Обычная объектная модель PythonОбщие и изменяемые данные (графовые структуры)

Ключевая мысль: Rc<RefCell<T>> даёт семантику, похожую на Python (общие, изменяемые данные), но включать её нужно явно. Принятое в Rust по умолчанию владение с перемещением быстрее и не несёт накладных расходов на подсчёт ссылок. Для графоподобных структур с циклами используйте Weak<T>, чтобы разрывать циклы ссылок: в отличие от Python, у Rc в Rust нет сборщика циклов.

📌 См. также: Гл. 13 — Конкурентность описывает Arc<Mutex<T>> для общего состояния в многопоточном коде.


Упражнения

🏋️ Упражнение: найдите ошибку заимствования (нажмите, чтобы раскрыть)

Задание: в следующем коде есть 3 ошибки borrow checker. Найдите каждую и исправьте их, не используя .clone():

fn main() {
    let mut names = vec!["Alice".to_string(), "Bob".to_string()];
    let first = &names[0];
    names.push("Charlie".to_string());
    println!("First: {first}");

    let greeting = make_greeting(names[0]);
    println!("{greeting}");
}

fn make_greeting(name: String) -> String {
    format!("Hello, {name}!")
}
🔑 Решение
fn main() {
    let mut names = vec!["Alice".to_string(), "Bob".to_string()];
    let first = &names[0];
    println!("First: {first}"); // Используем заимствование ДО изменения
    names.push("Charlie".to_string()); // Теперь безопасно — живого неизменяемого заимствования нет

    let greeting = make_greeting(&names[0]); // Передаём ссылку, а не владение
    println!("{greeting}");
}

fn make_greeting(name: &str) -> String { // Принимаем &str, а не String
    format!("Hello, {name}!")
}

Исправленные ошибки:

  1. Неизменяемое заимствование и изменение: first заимствует names, а затем push его изменяет. Исправление: использовать first до push.
  2. Перемещение из Vec: names[0] пытается переместить String из Vec, а это запрещено. Исправление: заимствовать через &names[0].
  3. Функция забирает владение: make_greeting(String) потребляет значение. Исправление: принимать &str.

8. Крейты и модули

Модули Rust и пакеты Python

Что вы узнаете: mod и use в сравнении с import, видимость (pub) в сравнении с приватностью Python, основанной на соглашениях, Cargo.toml в сравнении с pyproject.toml, crates.io в сравнении с PyPI, а также рабочие пространства в сравнении с монорепозиториями.

Сложность: 🟢 Начальный

Система модулей Python

# Python — файлы являются модулями, каталоги с __init__.py — пакетами

# myproject/
# ├── __init__.py          # Делает каталог пакетом
# ├── main.py
# ├── utils/
# │   ├── __init__.py      # Делает utils подпакетом
# │   ├── helpers.py
# │   └── validators.py
# └── models/
#     ├── __init__.py
#     ├── user.py
#     └── product.py

# Импорт:
from myproject.utils.helpers import format_name
from myproject.models.user import User
import myproject.utils.validators as validators

Система модулей Rust

#![allow(unused)]
fn main() {
// Rust — объявления mod создают дерево модулей, файлы содержат код

// src/
// ├── main.rs             # Корень крейта — объявляет модули
// ├── utils/
// │   ├── mod.rs           # Объявление модуля (как __init__.py)
// │   ├── helpers.rs
// │   └── validators.rs
// └── models/
//     ├── mod.rs
//     ├── user.rs
//     └── product.rs

// В src/main.rs:
mod utils;       // Говорит Rust искать src/utils/mod.rs
mod models;      // Говорит Rust искать src/models/mod.rs

use utils::helpers::format_name;
use models::user::User;

// В src/utils/mod.rs:
pub mod helpers;      // Объявляет модуль helpers из helpers.rs и делает его публичным
pub mod validators;   // Объявляет модуль validators из validators.rs и делает его публичным
}
graph TD
    A["main.rs<br/>(корень крейта)"] --> B["mod utils"]
    A --> C["mod models"]
    B --> D["utils/mod.rs"]
    D --> E["helpers.rs"]
    D --> F["validators.rs"]
    C --> G["models/mod.rs"]
    G --> H["user.rs"]
    G --> I["product.rs"]
    style A fill:#d4edda,stroke:#28a745
    style D fill:#fff3cd,stroke:#ffc107
    style G fill:#fff3cd,stroke:#ffc107

Аналог в Python: думайте о mod.rs как о __init__.py: он объявляет, что экспортирует модуль. Корень крейта (main.rs / lib.rs) похож на __init__.py вашего верхнеуровневого пакета.

Ключевые различия

ПонятиеPythonRust
Модуль = файл✅ АвтоматическиНужно объявить через mod
Пакет = каталог__init__.pymod.rs
Публично по умолчанию✅ Всё❌ По умолчанию приватно
Сделать публичнымСоглашение с префиксом _Ключевое слово pub
Синтаксис импортаfrom x import yuse x::y;
Импорт всегоfrom x import *use x::*; (не рекомендуется)
Относительный импортfrom . import siblinguse super::sibling;
Реэкспорт__all__ или явноpub use inner::Thing;

Видимость: приватно по умолчанию

# Python — «здесь все взрослые»
class User:
    def __init__(self):
        self.name = "Alice"       # Публичное (по соглашению)
        self._age = 30            # «Приватное» (соглашение: одно подчёркивание)
        self.__secret = "тсс"     # Искажённое имя (не по-настоящему приватное)

# Ничто не мешает обратиться к _age или даже к __secret
print(user._age)                  # Работает
print(user._User__secret)        # Тоже работает (искажение имён)
#![allow(unused)]
fn main() {
// Rust — приватность гарантируется компилятором
pub struct User {
    pub name: String,      // Публичное — доступно всем
    age: i32,              // Приватное — доступно только этому модулю
}

impl User {
    pub fn new(name: &str, age: i32) -> Self {
        User { name: name.to_string(), age }
    }

    pub fn age(&self) -> i32 {   // Публичный геттер
        self.age
    }

    fn validate(&self) -> bool { // Приватный метод
        self.age > 0
    }
}

// Снаружи модуля:
let user = User::new("Alice", 30);
println!("{}", user.name);        // ✅ Публичное
// println!("{}", user.age);      // ❌ Ошибка компиляции: поле приватное
println!("{}", user.age());       // ✅ Публичный метод (геттер)
}

Крейты и пакеты PyPI

Пакеты Python (PyPI)

# Python
pip install requests           # Установка из PyPI
pip install "requests>=2.28"   # Ограничение версии
pip freeze > requirements.txt  # Зафиксировать версии
pip install -r requirements.txt # Воссоздать окружение

Крейты Rust (crates.io)

# Rust
cargo add reqwest              # Установка из crates.io (добавляет в Cargo.toml)
cargo add reqwest@0.12         # Ограничение версии
# Cargo.lock создаётся автоматически, никаких ручных действий
cargo build                    # Загружает и компилирует зависимости

Cargo.toml и pyproject.toml

# Rust — Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }  # С флагами функций
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"

[dev-dependencies]
mockall = "0.13"

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

Библиотека PythonКрейт RustНазначение
requestsreqwestHTTP-клиент
json (stdlib)serde_jsonРазбор JSON
pydanticserdeСериализация и валидация
pathlibstd::path (stdlib)Работа с путями
os / shutilstd::fs (stdlib)Операции с файлами
reregexРегулярные выражения
loggingtracing / logЛогирование
click / argparseclapРазбор аргументов CLI
asynciotokioАсинхронный рантайм
datetimechronoДата и время
pytestВстроенное + rstestТестирование
dataclasses#[derive(...)]Структуры данных
typing.ProtocolТрейтыСтруктурная типизация
subprocessstd::process (stdlib)Запуск внешних команд
sqlite3rusqliteSQLite
sqlalchemydiesel / sqlxORM / инструментарий SQL
fastapiaxum / actix-webВеб-фреймворк

Рабочие пространства и монорепозитории

Монорепозиторий Python (типичный)

# Монорепозиторий Python (разные подходы, стандарта нет)
myproject/
├── pyproject.toml           # Корневой проект
├── packages/
│   ├── core/
│   │   ├── pyproject.toml   # У каждого пакета своя конфигурация
│   │   └── src/core/...
│   ├── api/
│   │   ├── pyproject.toml
│   │   └── src/api/...
│   └── cli/
│       ├── pyproject.toml
│       └── src/cli/...
# Инструменты: poetry workspaces, pip -e ., uv workspaces — стандарта нет

Рабочее пространство Rust

# Rust — Cargo.toml в корне
[workspace]
members = [
    "core",
    "api",
    "cli",
]

# Общие зависимости для всего рабочего пространства
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Структура рабочего пространства Rust — стандартизирована, встроена в Cargo
myproject/
├── Cargo.toml               # Корень рабочего пространства
├── Cargo.lock               # Единый lock-файл для всех крейтов
├── core/
│   ├── Cargo.toml            # [dependencies] serde.workspace = true
│   └── src/lib.rs
├── api/
│   ├── Cargo.toml
│   └── src/lib.rs
└── cli/
    ├── Cargo.toml
    └── src/main.rs
# Команды рабочего пространства
cargo build                  # Собрать всё
cargo test                   # Протестировать всё
cargo build -p core          # Собрать только крейт core
cargo test -p api            # Протестировать только крейт api
cargo clippy --all           # Проверить линтером всё

Ключевая мысль: рабочие пространства в Rust — полноценная возможность, встроенная в Cargo. Монорепозиториям Python нужны сторонние инструменты (poetry, uv, pants) с разным уровнем поддержки. В рабочем пространстве Rust все крейты используют один Cargo.lock, что обеспечивает согласованные версии зависимостей во всём проекте.


Упражнения

🏋️ Упражнение: видимость модулей (нажмите, чтобы раскрыть)

Задание: для этой структуры модулей предскажите, какие строки компилируются, а какие нет:

mod kitchen {
    fn secret_recipe() -> &'static str { "42 специи" }
    pub fn menu() -> &'static str { "Фирменное блюдо дня" }

    pub mod staff {
        pub fn cook() -> String {
            format!("Готовим с помощью {}", super::secret_recipe())
        }
    }
}

fn main() {
    println!("{}", kitchen::menu());             // Строка A
    println!("{}", kitchen::secret_recipe());     // Строка B
    println!("{}", kitchen::staff::cook());       // Строка C
}
🔑 Решение
  • Строка A: ✅ Компилируется: menu() объявлена как pub
  • Строка B: ❌ Ошибка компиляции: secret_recipe() приватна для kitchen
  • Строка C: ✅ Компилируется: staff::cook() объявлена как pub, а cook() может обращаться к secret_recipe() через super:: (дочерние модули видят приватные элементы родителя)

Ключевой вывод: в Rust дочерние модули видят приватные элементы родителя (как соглашение _private в Python, но с гарантией компилятора). Посторонние — нет. Это противоположность Python, где _private — лишь подсказка.


9. Обработка ошибок

Исключения и Result

Что вы узнаете: Result<T, E> в сравнении с try/except, оператор ? для краткой передачи ошибок, собственные типы ошибок с thiserror, anyhow для приложений и то, почему явные ошибки предотвращают скрытые баги.

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

Для разработчиков на Python это одна из самых больших смен мышления. Python использует исключения: ошибку можно выбросить откуда угодно и перехватить где угодно (или не перехватывать вовсе). Rust использует Result<T, E>: ошибки — это значения, которые нужно явно обработать.

Обработка исключений в Python

# Python — исключения могут быть выброшены откуда угодно
import json

def load_config(path: str) -> dict:
    try:
        with open(path) as f:
            data = json.load(f)     # Может выбросить JSONDecodeError
            if "version" not in data:
                raise ValueError("Отсутствует поле version")
            return data
    except FileNotFoundError:
        print(f"Файл конфигурации не найден: {path}")
        return {}
    except json.JSONDecodeError as e:
        print(f"Некорректный JSON: {e}")
        return {}
    # Какие ещё исключения может выбросить эта функция?
    # IOError? PermissionError? UnicodeDecodeError?
    # По сигнатуре функции этого не понять!

Обработка ошибок через Result в Rust

#![allow(unused)]
fn main() {
// Rust — ошибки являются возвращаемыми значениями, видимыми в сигнатуре функции
use std::fs;
use serde_json::Value;

fn load_config(path: &str) -> Result<Value, ConfigError> {
    let contents = fs::read_to_string(path)    // Возвращает Result
        .map_err(|e| ConfigError::FileError(e.to_string()))?;

    let data: Value = serde_json::from_str(&contents)  // Возвращает Result
        .map_err(|e| ConfigError::ParseError(e.to_string()))?;

    if data.get("version").is_none() {
        return Err(ConfigError::MissingField("version".to_string()));
    }

    Ok(data)
}

#[derive(Debug)]
enum ConfigError {
    FileError(String),
    ParseError(String),
    MissingField(String),
}
}

Ключевые различия

Python:                                 Rust:
─────────                               ─────
- Ошибки — это исключения (выбрасываются)   - Ошибки — это значения (возвращаются)
- Скрытый поток управления (раскрутка стека) - Явный поток управления (оператор ?)
- По сигнатуре не понять, какие ошибки      - Ошибки ОБЯЗАНЫ быть видны в типе возврата
- Необработанные исключения роняют программу в рантайме - Необработанные Result дают предупреждения компилятора (их всегда нужно обрабатывать)
- try/except необязательны                 - Обработка Result обязательна
- Широкий except ловит всё                 - Ветки match исчерпывающие

Два варианта Result

#![allow(unused)]
fn main() {
// У Result<T, E> ровно два варианта:
enum Result<T, E> {
    Ok(T),    // Успех — содержит значение (как return в Python)
    Err(E),   // Неудача — содержит ошибку (как выброшенное исключение в Python)
}

// Использование Result:
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Деление на ноль".to_string())  // Как: raise ValueError("...")
    } else {
        Ok(a / b)                             // Как: return a / b
    }
}

// Обработка Result — как try/except, только явная
match divide(10.0, 0.0) {
    Ok(result) => println!("Результат: {result}"),
    Err(msg) => println!("Ошибка: {msg}"),
}
}

Оператор ?

Оператор ? — это аналог в Rust того, что исключения всплывают вверх по стеку вызовов, только он виден и явен.

Python: неявное распространение

# Python — исключения тихо всплывают вверх по стеку вызовов
def read_username() -> str:
    with open("config.txt") as f:      # FileNotFoundError всплывает
        return f.readline().strip()    # IOError всплывает

def greet():
    name = read_username()             # Если здесь выброшено исключение, greet() тоже его выбросит
    print(f"Привет, {name}!")          # При ошибке эта строка пропускается

# Распространение ошибок НЕВИДИМО — чтобы узнать, какие исключения могут выйти наружу,
# нужно читать реализацию.

Rust: явное распространение с помощью ?

#![allow(unused)]
fn main() {
// Rust — ? передаёт ошибки дальше, и это видно и в коде, и в сигнатуре
use std::fs;
use std::io;

fn read_username() -> Result<String, io::Error> {
    let contents = fs::read_to_string("config.txt")?;  // ? = передать дальше при Err
    Ok(contents.lines().next().unwrap_or("").to_string())
}

fn greet() -> Result<(), io::Error> {
    let name = read_username()?;       // ? = если Err, сразу вернуть Err
    println!("Привет, {name}!");        // Достигается только при Ok
    Ok(())
}

// ? означает: «если здесь Err, немедленно вернуть его ИЗ ЭТОЙ функции».
// Это похоже на распространение исключений в Python, но:
// 1. Это видно (вы видите ?)
// 2. Это есть в типе возврата (Result<..., io::Error>)
// 3. Компилятор следит, чтобы ошибка была обработана где-то
}

Цепочки с ?

# Python — несколько операций, которые могут завершиться ошибкой
def process_file(path: str) -> dict:
    with open(path) as f:                    # Может завершиться ошибкой
        text = f.read()                       # Может завершиться ошибкой
    data = json.loads(text)                   # Может завершиться ошибкой
    validate(data)                            # Может завершиться ошибкой
    return transform(data)                    # Может завершиться ошибкой
    # Любая из них может выбросить исключение, и тип исключения у каждой свой!
#![allow(unused)]
fn main() {
// Rust — та же цепочка, но явная
fn process_file(path: &str) -> Result<Data, AppError> {
    let text = fs::read_to_string(path)?;     // ? передаёт io::Error дальше
    let data: Value = serde_json::from_str(&text)?;  // ? передаёт ошибку serde дальше
    let validated = validate(&data)?;          // ? передаёт ошибку валидации дальше
    let result = transform(&validated)?;       // ? передаёт ошибку преобразования дальше
    Ok(result)
}
// Каждый ? — это потенциальный досрочный возврат, и все они видны!
}
flowchart TD
    A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"]
    A -->|Err| X["Возврат Err(io::Error)"]
    B -->|Ok| C["validate(&data)?"]
    B -->|Err| Y["Возврат Err(serde::Error)"]
    C -->|Ok| D["transform(&validated)?"]
    C -->|Err| Z["Возврат Err(ValidationError)"]
    D -->|Ok| E["Ok(result) ✅"]
    D -->|Err| W["Возврат Err(TransformError)"]
    style E fill:#d4edda,stroke:#28a745
    style X fill:#f8d7da,stroke:#dc3545
    style Y fill:#f8d7da,stroke:#dc3545
    style Z fill:#f8d7da,stroke:#dc3545
    style W fill:#f8d7da,stroke:#dc3545

Каждый ? — это точка выхода. В отличие от try/except в Python, где без чтения документации не видно, какая строка может выбросить исключение.

📌 См. также: Гл. 15 — Паттерны миграции рассказывает о переводе паттернов try/except из Python на Rust в реальных проектах.


Собственные типы ошибок с thiserror

graph TD
    AE["AppError (перечисление)"] --> NF["NotFound<br/>{ entity, id }"]
    AE --> VE["Validation<br/>{ field, message }"]
    AE --> IO["Io(std::io::Error)<br/>#[from]"]
    AE --> JSON["Json(serde_json::Error)<br/>#[from]"]
    IO2["std::io::Error"] -->|"автопреобразование через From"| IO
    JSON2["serde_json::Error"] -->|"автопреобразование через From"| JSON
    style AE fill:#d4edda,stroke:#28a745
    style NF fill:#fff3cd
    style VE fill:#fff3cd
    style IO fill:#fff3cd
    style JSON fill:#fff3cd
    style IO2 fill:#f8d7da
    style JSON2 fill:#f8d7da

Атрибут #[from] автоматически генерирует impl From<io::Error> for AppError, поэтому ? сам превращает ошибки библиотек в ошибки вашего приложения.

Собственные исключения в Python

# Python — собственные классы исключений
class AppError(Exception):
    pass

class NotFoundError(AppError):
    def __init__(self, entity: str, id: int):
        self.entity = entity
        self.id = id
        super().__init__(f"{entity} с id {id} не найден")

class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(f"Ошибка валидации поля {field}: {message}")

# Использование:
def find_user(user_id: int) -> dict:
    if user_id not in users:
        raise NotFoundError("Пользователь", user_id)
    return users[user_id]

Собственные ошибки в Rust с thiserror

#![allow(unused)]
fn main() {
// Rust — перечисления ошибок с thiserror (самый популярный подход)
// Cargo.toml: thiserror = "2"

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("{entity} с id {id} не найден")]
    NotFound { entity: String, id: i64 },

    #[error("Ошибка валидации поля {field}: {message}")]
    Validation { field: String, message: String },

    #[error("Ошибка ввода-вывода: {0}")]
    Io(#[from] std::io::Error),        // Автопреобразование из io::Error

    #[error("Ошибка JSON: {0}")]
    Json(#[from] serde_json::Error),   // Автопреобразование из ошибки serde
}

// Использование:
fn find_user(user_id: i64) -> Result<User, AppError> {
    users.get(&user_id)
        .cloned()
        .ok_or(AppError::NotFound {
            entity: "Пользователь".to_string(),
            id: user_id,
        })
}

// Атрибут #[from] означает, что ? автоматически преобразует io::Error в AppError::Io
fn load_users(path: &str) -> Result<Vec<User>, AppError> {
    let data = fs::read_to_string(path)?;  // io::Error → AppError::Io автоматически
    let users: Vec<User> = serde_json::from_str(&data)?;  // → AppError::Json
    Ok(users)
}
}

Краткая справка по обработке ошибок

PythonRustПримечания
raise ValueError("msg")return Err(AppError::Validation {...})Явный возврат
try: ... except:match result { Ok(v) => ..., Err(e) => ... }Исчерпывающе
except ValueError as e:Err(AppError::Validation { .. }) =>Сопоставление с образцом
raise ... from eАтрибут #[from] или .map_err()Цепочка ошибок
finally:Трейт Drop (автоматически)Детерминированная очистка
with open(...):Освобождение по области видимости (автоматически)Паттерн RAII
Исключение распространяется молча? распространяет явноВсегда в типе возврата
isinstance(e, ValueError)matches!(e, AppError::Validation {..})Проверка типа

Упражнения

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

Задание: напишите функцию parse_port(s: &str) -> Result<u16, String>, которая:

  1. Отклоняет пустые строки с ошибкой "пустой ввод"
  2. Разбирает строку в u16, преобразуя ошибку разбора в "некорректное число: {исходная_ошибка}"
  3. Отклоняет порты ниже 1024 с ошибкой "порт {n} привилегированный"

Вызовите её с "", "hello", "80" и "8080" и выведите результаты.

🔑 Решение
fn parse_port(s: &str) -> Result<u16, String> {
    if s.is_empty() {
        return Err("пустой ввод".to_string());
    }
    let port: u16 = s.parse().map_err(|e| format!("некорректное число: {e}"))?;
    if port < 1024 {
        return Err(format!("порт {port} привилегированный"));
    }
    Ok(port)
}

fn main() {
    for input in ["", "hello", "80", "8080"] {
        match parse_port(input) {
            Ok(port) => println!("✅ {input} → {port}"),
            Err(e) => println!("❌ {input:?} → {e}"),
        }
    }
}

Ключевой вывод: ? вместе с .map_err() — это аналог в Rust конструкции try/except ValueError as e: raise ConfigError(...) from e. Каждый путь ошибки виден в типе возврата.


10. Трейты и обобщения

Трейты и утиная типизация

Что вы узнаете: трейты как явные контракты (в отличие от утиной типизации Python), Protocol (PEP 544) ≈ трейт, ограничения обобщений через предложения where, трейт-объекты (dyn Trait) в сравнении со статической диспетчеризацией и распространённые трейты стандартной библиотеки.

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

Именно здесь система типов Rust по-настоящему раскрывается для разработчиков на Python. Утиная типизация Python говорит: «если это ходит как утка и крякает как утка, значит, это утка». Трейты Rust говорят: «я точно скажу, какое поведение утки мне нужно, и это будет проверено на этапе компиляции».

Утиная типизация в Python

# Python — утиная типизация: подходит всё, у чего есть нужные методы
def total_area(shapes):
    """Работает с любым объектом, у которого есть метод .area()."""
    return sum(shape.area() for shape in shapes)

class Circle:
    def __init__(self, radius): self.radius = radius
    def area(self): return 3.14159 * self.radius ** 2

class Rectangle:
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h

# Работает во время выполнения, наследование не нужно!
shapes = [Circle(5), Rectangle(3, 4)]
print(total_area(shapes))  # 90.54

# А что, если у объекта нет .area()?
class Dog:
    def bark(self): return "Гав!"

total_area([Dog()])  # 💥 AttributeError: 'Dog' has no attribute 'area'
# Ошибка возникает во ВРЕМЯ ВЫПОЛНЕНИЯ, а не при определении

Трейты Rust: явная утиная типизация

#![allow(unused)]
fn main() {
// Rust — трейты делают контракт «утки» явным
trait HasArea {
    fn area(&self) -> f64;      // Любой тип, реализующий этот трейт, имеет .area()
}

struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl HasArea for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

// Ограничение трейтом явное, компилятор проверяет его на этапе компиляции
fn total_area(shapes: &[&dyn HasArea]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

// Использование:
let shapes: Vec<&dyn HasArea> = vec![&Circle { radius: 5.0 }, &Rectangle { width: 3.0, height: 4.0 }];
println!("{}", total_area(&shapes));  // 90.54

// struct Dog;
// total_area(&[&Dog {}]);  // ❌ Ошибка компиляции: Dog не реализует HasArea
}

Ключевая мысль: утиная типизация Python откладывает ошибки до времени выполнения. Трейты Rust ловят их на этапе компиляции. Гибкость та же, а ошибки обнаруживаются раньше.


Протоколы (PEP 544) и трейты

Python 3.8 ввёл Protocol (PEP 544) для структурной типизации: это ближайшее к трейтам Rust понятие в Python.

Protocol в Python

# Python — Protocol (структурная типизация, как трейты Rust)
from typing import Protocol, runtime_checkable

@runtime_checkable
class Printable(Protocol):
    def to_string(self) -> str: ...

class User:
    def __init__(self, name: str):
        self.name = name
    def to_string(self) -> str:
        return f"User({self.name})"

class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price
    def to_string(self) -> str:
        return f"Product({self.name}, ${self.price:.2f})"

def print_all(items: list[Printable]) -> None:
    for item in items:
        print(item.to_string())

# Работает, потому что у User и Product есть to_string()
print_all([User("Alice"), Product("Widget", 9.99)])

# НО: mypy это проверяет, а среда выполнения Python — НЕТ
# print_all([42])  # mypy предупредит, но Python запустит это и упадёт

Трейт Rust (аналог, но с принуждением!)

#![allow(unused)]
fn main() {
// Rust — трейты проверяются на этапе компиляции
trait Printable {
    fn to_string(&self) -> String;
}

struct User { name: String }
struct Product { name: String, price: f64 }

impl Printable for User {
    fn to_string(&self) -> String {
        format!("User({})", self.name)
    }
}

impl Printable for Product {
    fn to_string(&self) -> String {
        format!("Product({}, ${:.2})", self.name, self.price)
    }
}

fn print_all(items: &[&dyn Printable]) {
    for item in items {
        println!("{}", item.to_string());
    }
}

// print_all(&[&42i32]);  // ❌ Ошибка компиляции: i32 не реализует Printable
}

Сравнительная таблица

СвойствоProtocol в PythonТрейт Rust
Структурная типизация✅ (неявно)❌ (нужен явный impl)
Когда проверяетсяВо время выполнения (или mypy)На этапе компиляции (всегда)
Реализации по умолчанию❌✅
Можно добавить к чужим типам❌✅ (в определённых пределах)
Несколько протоколов✅✅ (несколько трейтов)
Ассоциированные типы❌✅
Ограничения обобщений✅ (с TypeVar)✅ (ограничения трейтов)

Ограничения обобщений

Обобщения в Python

# Python — TypeVar для обобщённых функций
from typing import TypeVar, Sequence

T = TypeVar('T')

def first(items: Sequence[T]) -> T | None:
    return items[0] if items else None

# Ограниченный TypeVar
from typing import SupportsFloat
T = TypeVar('T', bound=SupportsFloat)

def average(items: Sequence[T]) -> float:
    return sum(float(x) for x in items) / len(items)

Обобщения Rust с ограничениями трейтов

#![allow(unused)]
fn main() {
// Rust — обобщения с ограничениями трейтов
fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

// С ограничениями трейтов — «T должен реализовывать эти трейты»
fn average<T>(items: &[T]) -> f64
where
    T: Into<f64> + Copy,   // T должен преобразовываться в f64 и быть копируемым
{
    let sum: f64 = items.iter().map(|&x| x.into()).sum();
    sum / items.len() as f64
}

// Несколько ограничений — «T должен реализовывать Display, Debug и Clone»
fn log_and_clone<T: std::fmt::Display + std::fmt::Debug + Clone>(item: &T) -> T {
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
    item.clone()
}

// Краткая запись с impl Trait (для простых случаев)
fn print_it(item: &impl std::fmt::Display) {
    println!("{}", item);
}
}

Краткая справка по обобщениям

PythonRustПримечания
TypeVar('T')<T>Обобщённый тип без ограничений
TypeVar('T', bound=X)<T: X>Обобщённый тип с ограничением
Union[int, str]enum или трейт-объектВ Rust нет объединений типов
Sequence[T]&[T] (срез)Заимствованная последовательность
Callable[[A], R]Fn(A) -> RТрейт функции
Optional[T]Option<T>Встроено в язык

Распространённые трейты стандартной библиотеки

Это аналоги «магических методов» Python в Rust: они определяют, как типы ведут себя в типичных ситуациях.

Display и Debug (вывод)

#![allow(unused)]
fn main() {
use std::fmt;

// Debug — как __repr__ (можно получить через derive)
#[derive(Debug)]
struct Point { x: f64, y: f64 }
// Теперь можно: println!("{:?}", point);

// Display — как __str__ (реализуется вручную)
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
// Теперь можно: println!("{}", point);
}

Трейты сравнения

#![allow(unused)]
fn main() {
// PartialEq — как __eq__
// Eq — полное равенство (f64 реализует PartialEq, но не Eq, потому что NaN != NaN)
// PartialOrd — как __lt__, __le__ и т. д.
// Ord — полный порядок

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
struct Student {
    name: String,
    grade: i32,
}

// Теперь студентов можно сравнивать, сортировать, использовать как ключи HashMap и клонировать
let mut students = vec![
    Student { name: "Charlie".into(), grade: 85 },
    Student { name: "Alice".into(), grade: 92 },
];
students.sort();  // Использует Ord: сортирует по имени, затем по оценке (порядок полей структуры)
}

Трейт Iterator

#![allow(unused)]
fn main() {
// Реализация Iterator — как __iter__/__next__ в Python
struct Countdown { value: i32 }

impl Iterator for Countdown {
    type Item = i32;       // Что выдаёт итератор

    fn next(&mut self) -> Option<Self::Item> {
        if self.value > 0 {
            self.value -= 1;
            Some(self.value + 1)
        } else {
            None             // Итерация завершена
        }
    }
}

// Использование:
for n in (Countdown { value: 5 }) {
    println!("{n}");  // 5, 4, 3, 2, 1
}
}

Общие трейты в одном взгляде

Трейт RustАналог в PythonНазначение
Display__str__Человекочитаемая строка
Debug__repr__Отладочная строка (можно получить через derive)
Clonecopy.deepcopyГлубокая копия
Copy(автокопирование int/float)Неявное копирование простых типов
PartialEq / Eq__eq__Сравнение на равенство
PartialOrd / Ord__lt__ и др.Упорядочивание
Hash__hash__Хешируемость (для ключей dict)
DefaultЗначения по умолчанию в __init__Значения по умолчанию
From / IntoПерегрузки __init__Преобразования типов
Iterator__iter__ / __next__Итерация
Drop__del__ / __exit__Очистка
Add, Sub, Mul__add__, __sub__, __mul__Перегрузка операторов
Index__getitem__Индексация через []
Deref(аналога нет)Разыменование умных указателей
Send / Sync(аналога нет)Маркеры потокобезопасности
flowchart TB
    subgraph Static ["Статическая диспетчеризация (impl Trait)"]
        G["fn notify(item: &impl Summary)"] --> M1["Скомпилировано: notify_Article()"]
        G --> M2["Скомпилировано: notify_Tweet()"]
        M1 --> O1["Встроено, без затрат"]
        M2 --> O2["Встроено, без затрат"]
    end
    subgraph Dynamic ["Динамическая диспетчеризация (dyn Trait)"]
        D["fn notify(item: &dyn Summary)"] --> VT["Поиск в таблице виртуальных методов (vtable)"]
        VT --> I1["Article::summarize()"]
        VT --> I2["Tweet::summarize()"]
    end
    Static ~~~ Dynamic
    style Static fill:#d4edda
    style Dynamic fill:#fff3cd

Аналог в Python: Python всегда использует динамическую диспетчеризацию (getattr во время выполнения). Rust по умолчанию использует статическую диспетчеризацию (мономорфизацию: компилятор генерирует специализированный код для каждого конкретного типа). dyn Trait используйте только тогда, когда нужен полиморфизм во время выполнения.

📌 См. также: Гл. 11 — Трейты From и Into подробно описывает трейты преобразования (From, Into, TryFrom).

Ассоциированные типы

Трейты Rust могут определять ассоциированные типы: заполнители типов, которые заполняет каждая реализация. У Python нет аналога:

#![allow(unused)]
fn main() {
// Iterator определяет ассоциированный тип 'Item'
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Countdown { remaining: u32 }

impl Iterator for Countdown {
    type Item = u32;  // Этот итератор выдаёт значения u32
    fn next(&mut self) -> Option<u32> {
        if self.remaining > 0 {
            self.remaining -= 1;
            Some(self.remaining)
        } else {
            None
        }
    }
}
}

В Python __iter__ / __next__ возвращают Any: нельзя объявить «этот итератор выдаёт int» и получить принудительную проверку (подсказки типов Iterator[int] носят лишь рекомендательный характер).

Перегрузка операторов: __add__ → impl Add

Python использует магические методы (__add__, __mul__). Rust использует реализации трейтов: та же идея, но с проверкой типов на этапе компиляции:

# Python
class Vec2:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vec2(self.x + other.x, self.y + other.y)  # Нет проверки типа 'other'
#![allow(unused)]
fn main() {
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }

impl Add for Vec2 {
    type Output = Vec2;  // Ассоциированный тип: что возвращает +?
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

let a = Vec2 { x: 1.0, y: 2.0 };
let b = Vec2 { x: 3.0, y: 4.0 };
let c = a + b;  // Безопасно по типам: допускается только Vec2 + Vec2
}

Ключевое отличие: __add__ в Python принимает любой other во время выполнения (типы приходится проверять вручную или получить TypeError). Трейт Add в Rust проверяет типы операндов на этапе компиляции: Vec2 + i32 — ошибка компиляции, если вы явно не напишете impl Add<i32> for Vec2.


Упражнения

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

Задание: определите трейт Summary с методом fn summarize(&self) -> String. Реализуйте его для двух структур: Article { title: String, body: String } и Tweet { username: String, content: String }. Затем напишите функцию fn notify(item: &impl Summary), которая выводит сводку.

🔑 Решение
trait Summary {
    fn summarize(&self) -> String;
}

struct Article { title: String, body: String }
struct Tweet { username: String, content: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} — {}...", self.title, &self.body[..20.min(self.body.len())])
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

fn notify(item: &impl Summary) {
    println!("📢 {}", item.summarize());
}

fn main() {
    let article = Article {
        title: "Rust is great".into(),
        body: "Here is why Rust beats Python for systems...".into(),
    };
    let tweet = Tweet {
        username: "rustacean".into(),
        content: "Just shipped my first crate!".into(),
    };
    notify(&article);
    notify(&tweet);
}

Ключевой вывод: &impl Summary — это аналог Protocol из Python с методом summarize. Но Rust проверяет это на этапе компиляции: передача типа, который не реализует Summary, — ошибка компиляции, а не AttributeError во время выполнения.


11. Трейты From и Into

Преобразование типов в Rust

Что вы узнаете: трейты From и Into для преобразований без накладных расходов, TryFrom для преобразований, которые могут завершиться ошибкой, как impl From<A> for B автоматически создаёт Into, и паттерны преобразования строк.

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

Python выполняет преобразования типов через вызовы конструкторов (int("42"), str(42), float("3.14")). Rust использует трейты From и Into для типобезопасных преобразований.

Преобразование типов в Python

# Python — явные конструкторы для преобразований
x = int("42")           # str → int (может выбросить ValueError)
s = str(42)             # int → str
f = float("3.14")       # str → float
lst = list((1, 2, 3))   # tuple → list

# Собственное преобразование через __init__ или методы класса
class Celsius:
    def __init__(self, temp: float):
        self.temp = temp

    @classmethod
    def from_fahrenheit(cls, f: float) -> "Celsius":
        return cls((f - 32.0) * 5.0 / 9.0)

c = Celsius.from_fahrenheit(212.0)  # 100.0°C

From/Into в Rust

#![allow(unused)]
fn main() {
// Rust — трейт From задаёт преобразования
// Реализация From<T> даёт вам Into<U> автоматически!

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

// Теперь работают оба варианта:
let c1 = Celsius::from(Fahrenheit(212.0));    // Явный From
let c2: Celsius = Fahrenheit(212.0).into();   // Into (выводится автоматически)

// Преобразования строк:
let s: String = String::from("hello");         // &str → String
let s: String = "hello".to_string();           // То же самое
let s: String = "hello".into();                // Тоже работает (From реализован)

let num: i64 = 42i32.into();                   // i32 → i64 (без потерь, поэтому From есть)
// let small: i32 = 42i64.into();              // ❌ i64 → i32 может потерять данные, From нет

// Для преобразований, которые могут завершиться ошибкой, используйте TryFrom:
let n: Result<i32, _> = "42".parse();          // str → i32 (может завершиться ошибкой)
let n: i32 = "42".parse().unwrap();            // Паника, если это не число
let n: i32 = "42".parse()?;                    // Передать ошибку дальше с помощью ?
}

Связь From и Into

flowchart TB
    A["impl From&lt;A&gt; for B"] -->|"генерирует автоматически"| B["impl Into&lt;B&gt; for A"]
    C["Celsius::from(Fahrenheit(212.0))"] ---|"то же, что"| D["Fahrenheit(212.0).into()"]
    style A fill:#d4edda
    style B fill:#d4edda

Правило большого пальца: всегда реализуйте From, а не Into напрямую. Реализация From<A> for B даёт вам Into<B> for A бесплатно.


Когда использовать From/Into

#![allow(unused)]
fn main() {
// Реализуйте From<T> для своих типов, чтобы API было удобнее:

#[derive(Debug)]
struct UserId(i64);

impl From<i64> for UserId {
    fn from(id: i64) -> Self {
        UserId(id)
    }
}

// Теперь функции могут принимать всё, что можно преобразовать в UserId:
fn find_user(id: impl Into<UserId>) -> Option<String> {
    let user_id = id.into();
    // ... логика поиска
    Some(format!("User #{:?}", user_id))
}

find_user(42i64);              // ✅ i64 автоматически преобразуется в UserId
find_user(UserId(42));         // ✅ UserId остаётся как есть
}

TryFrom: преобразования, которые могут завершиться ошибкой

Не все преобразования могут быть успешными. Python выбрасывает исключения; Rust использует TryFrom, который возвращает Result:

# Python — преобразования, которые могут завершиться ошибкой, выбрасывают исключения
try:
    port = int("not_a_number")   # ValueError
except ValueError as e:
    print(f"Некорректно: {e}")

# Собственная валидация в __init__
class Port:
    def __init__(self, value: int):
        if not (1 <= value <= 65535):
            raise ValueError(f"Некорректный порт: {value}")
        self.value = value

try:
    p = Port(99999)  # ValueError во время выполнения
except ValueError:
    pass
#![allow(unused)]
fn main() {
use std::num::ParseIntError;

// TryFrom для встроенных типов
let n: Result<i32, ParseIntError> = "42".try_into();   // Ok(42)
let n: Result<i32, ParseIntError> = "bad".try_into();  // Err(...)

// Собственный TryFrom для валидации
#[derive(Debug)]
struct Port(u16);

#[derive(Debug)]
enum PortError {
    Zero,
}

impl TryFrom<u16> for Port {
    type Error = PortError;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        match value {
            0 => Err(PortError::Zero),
            1..=65535 => Ok(Port(value)),
        }
    }
}

impl std::fmt::Display for PortError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PortError::Zero => write!(f, "порт не может быть нулём"),
        }
    }
}

// Использование:
let p: Result<Port, _> = 8080u16.try_into();   // Ok(Port(8080))
let p: Result<Port, _> = 0u16.try_into();       // Err(PortError::Zero)
}

Ментальная модель Python → Rust: TryFrom — это __init__, который проверяет данные и может завершиться ошибкой. Но вместо выбрасывания исключения он возвращает Result, поэтому вызывающий код обязан обработать случай ошибки.


Паттерны преобразования строк

Строки — самый частый источник путаницы при преобразованиях для разработчиков на Python:

#![allow(unused)]
fn main() {
// String → &str (заимствование, бесплатно)
let s = String::from("hello");
let r: &str = &s;              // Автоматическое приведение через Deref
let r: &str = s.as_str();     // Явно

// &str → String (выделение памяти, стоит ресурсов)
let r: &str = "hello";
let s1 = String::from(r);     // Трейт From
let s2 = r.to_string();       // Трейт ToString (через Display)
let s3: String = r.into();    // Трейт Into

// Число → String
let s = 42.to_string();       // "42" — как str(42) в Python
let s = format!("{:.2}", 3.14); // "3.14" — как f"{3.14:.2f}" в Python

// Строка → число
let n: i32 = "42".parse().unwrap();       // как int("42") в Python
let f: f64 = "3.14".parse().unwrap();     // как float("3.14") в Python

// Собственные типы → String (реализуйте Display)
use std::fmt;

struct Point { x: f64, y: f64 }

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

let p = Point { x: 1.0, y: 2.0 };
println!("{p}");                // (1, 2) — как __str__ в Python
let s = p.to_string();         // Тоже работает! Display даёт ToString бесплатно.
}

Краткая справка по преобразованиям

PythonRustПримечания
str(x)x.to_string()Требует реализации Display
int("42")"42".parse::<i32>()Возвращает Result
float("3.14")"3.14".parse::<f64>()Возвращает Result
list(iter)iter.collect::<Vec<_>>()Нужна аннотация типа
dict(pairs)pairs.collect::<HashMap<_,_>>()Нужна аннотация типа
bool(x)Прямого аналога нетИспользуйте явные проверки
MyClass(x)MyClass::from(x)Реализуйте From<T>
MyClass(x) (с валидацией)MyClass::try_from(x)?Реализуйте TryFrom<T>

Цепочки преобразований и обработка ошибок

В реальном коде часто встречаются цепочки из нескольких преобразований. Сравните подходы:

# Python — цепочка преобразований с try/except
def parse_config(raw: str) -> tuple[str, int]:
    try:
        host, port_str = raw.split(":")
        port = int(port_str)
        if not (1 <= port <= 65535):
            raise ValueError(f"Некорректный порт: {port}")
        return (host, port)
    except (ValueError, AttributeError) as e:
        raise ConfigError(f"Некорректная конфигурация: {e}") from e
fn parse_config(raw: &str) -> Result<(String, u16), String> {
    let (host, port_str) = raw
        .split_once(':')
        .ok_or_else(|| "отсутствует разделитель ':'".to_string())?;

    let port: u16 = port_str
        .parse()
        .map_err(|e| format!("некорректный порт: {e}"))?;

    if port == 0 {
        return Err("порт не может быть нулём".to_string());
    }

    Ok((host.to_string(), port))
}

fn main() {
    match parse_config("localhost:8080") {
        Ok((host, port)) => println!("Подключение к {host}:{port}"),
        Err(e) => eprintln!("Ошибка конфигурации: {e}"),
    }
}

Ключевая мысль: каждый ? — это видимая точка выхода. В Python любая строка внутри try может оказаться той, что выбросит исключение, а в Rust ошибиться могут только строки, оканчивающиеся на ?.

📌 См. также: Гл. 9 — Обработка ошибок подробно описывает Result, ? и собственные типы ошибок с thiserror.


Упражнения

🏋️ Упражнение: библиотека преобразования температур (нажмите, чтобы раскрыть)

Задание: создайте мини-библиотеку преобразования температур:

  1. Определите структуры Celsius(f64), Fahrenheit(f64) и Kelvin(f64)
  2. Реализуйте From<Celsius> for Fahrenheit и From<Celsius> for Kelvin
  3. Реализуйте TryFrom<f64> for Kelvin, который отклоняет значения ниже абсолютного нуля (-273.15°C = 0K)
  4. Реализуйте Display для всех трёх типов (например, "100.00°C")
🔑 Решение
use std::fmt;

struct Celsius(f64);
struct Fahrenheit(f64);
struct Kelvin(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl From<Celsius> for Kelvin {
    fn from(c: Celsius) -> Self {
        Kelvin(c.0 + 273.15)
    }
}

#[derive(Debug)]
struct BelowAbsoluteZero;

impl fmt::Display for BelowAbsoluteZero {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "температура ниже абсолютного нуля")
    }
}

impl TryFrom<f64> for Kelvin {
    type Error = BelowAbsoluteZero;

    fn try_from(value: f64) -> Result<Self, Self::Error> {
        if value < 0.0 {
            Err(BelowAbsoluteZero)
        } else {
            Ok(Kelvin(value))
        }
    }
}

impl fmt::Display for Celsius    { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}°C", self.0) } }
impl fmt::Display for Fahrenheit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}°F", self.0) } }
impl fmt::Display for Kelvin     { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}K",  self.0) } }

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = Celsius(100.0).into();
    let k: Kelvin = Celsius(100.0).into();
    println!("{boiling} = {f} = {k}");

    match Kelvin::try_from(-10.0) {
        Ok(k) => println!("{k}"),
        Err(e) => println!("Ошибка: {e}"),
    }
}

Ключевой вывод: From отвечает за преобразования, которые всегда успешны (Celsius → Fahrenheit работает всегда). TryFrom отвечает за те, которые могут не удаться (отрицательный Kelvin невозможен). Python смешивает оба варианта в __init__, а Rust делает различие явным в системе типов.


12. Замыкания и итераторы

Замыкания Rust и лямбды Python

Что вы узнаете: многострочные замыкания (а не только однострочные лямбды), семантику захвата Fn/FnMut/FnOnce, цепочки итераторов в сравнении со списочными включениями, map/filter/fold и основы macro_rules!.

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

Замыкания и лямбды в Python

# Python — лямбды это анонимные функции из одного выражения
double = lambda x: x * 2
result = double(5)  # 10

# Полноценные замыкания захватывают переменные из окружающей области видимости:
def make_adder(n):
    def adder(x):
        return x + n    # Захватывает `n` из внешней области видимости
    return adder

add_5 = make_adder(5)
print(add_5(10))  # 15

# Функции высшего порядка:
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

Замыкания в Rust

#![allow(unused)]
fn main() {
// Rust — замыкания используют синтаксис |аргументы| тело
let double = |x: i32| x * 2;
let result = double(5);  // 10

// Замыкания захватывают переменные из окружающей области видимости:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n    // `move` передаёт владение `n` внутрь замыкания
}

let add_5 = make_adder(5);
println!("{}", add_5(10));  // 15

// Функции высшего порядка с итераторами:
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<i32> = numbers.iter().filter(|&&x| x % 2 == 0).copied().collect();
}

Сравнение синтаксиса замыканий

Python:                              Rust:
─────────                            ─────
lambda x: x * 2                      |x| x * 2
lambda x, y: x + y                   |x, y| x + y
lambda: 42                           || 42

# Многострочное
def f(x):                            |x| {
    y = x * 2                            let y = x * 2;
    return y + 1                         y + 1
                                      }

Захват в замыканиях: чем отличается Rust

# Python — замыкания захватывают по ссылке (позднее связывание!)
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2] — сюрприз! Все захватили один и тот же `i`

# Исправление через трюк со значением по умолчанию:
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]
#![allow(unused)]
fn main() {
// Rust — замыкания захватывают корректно (ловушки позднего связывания нет)
let funcs: Vec<Box<dyn Fn() -> i32>> = (0..3)
    .map(|i| Box::new(move || i) as Box<dyn Fn() -> i32>)
    .collect();

let results: Vec<i32> = funcs.iter().map(|f| f()).collect();
println!("{:?}", results);  // [0, 1, 2] — правильно!

// `move` захватывает КОПИЮ `i` для каждого замыкания, никаких сюрпризов позднего связывания.
}

Три трейта замыканий

#![allow(unused)]
fn main() {
// Замыкания Rust реализуют один или несколько из этих трейтов:

// Fn — можно вызывать много раз, не изменяет захваченное (самый распространённый)
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }

// FnMut — можно вызывать много раз, МОЖЕТ изменять захваченное
fn apply_mut(mut f: impl FnMut(i32) -> i32, x: i32) -> i32 { f(x) }

// FnOnce — можно вызвать ТОЛЬКО ОДИН раз (потребляет захваченное)
fn apply_once(f: impl FnOnce() -> String) -> String { f() }

// В Python аналога нет: замыкания всегда ведут себя как Fn.
// В Rust компилятор сам определяет, какой трейт использовать.
}

Итераторы и генераторы

Генераторы в Python

# Python — генераторы с yield
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Ленивые: значения вычисляются по требованию
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]

# Генераторные выражения — как ленивые списочные включения
squares = (x ** 2 for x in range(1000000))  # Без выделения памяти
first_5 = [next(squares) for _ in range(5)]

Итераторы Rust

#![allow(unused)]
fn main() {
// Rust — трейт Iterator (похожая концепция, другой синтаксис)
struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.a;
        self.a = self.b;
        self.b = current + self.b;
        Some(current)
    }
}

// Ленивые: значения вычисляются по требованию (как генераторы Python)
let first_10: Vec<u64> = Fibonacci::new().take(10).collect();

// Цепочки итераторов — как генераторные выражения
let squares: Vec<u64> = (0..1_000_000u64).map(|x| x * x).take(5).collect();
}

Списочные включения и цепочки итераторов

Этот раздел сопоставляет синтаксис списочных включений Python с цепочками итераторов Rust.

Списочные включения → map/filter/collect

# Списочные включения в Python:
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
names = [user.name for user in users if user.active]
pairs = [(x, y) for x in range(3) for y in range(3)]
flat = [item for sublist in nested for item in sublist]
flowchart LR
    A["Источник<br/>[1,2,3,4,5]"] -->|".iter()"| B["Итератор"]
    B -->|".filter(x: x%2==0)"| C["[2, 4]"]
    C -->|".map(x: x*x)"| D["[4, 16]"]
    D -->|".collect()"| E["Vec&lt;i32&gt;<br/>[4, 16]"]
    style A fill:#ffeeba
    style E fill:#d4edda

Ключевая мысль: итераторы Rust ленивые: ничего не происходит до .collect(). Генераторы Python работают похоже, но списочные включения вычисляются сразу (энергично).

#![allow(unused)]
fn main() {
// Цепочки итераторов Rust:
let squares: Vec<i32> = (0..10).map(|x| x * x).collect();
let evens: Vec<i32> = (0..20).filter(|x| x % 2 == 0).collect();
let names: Vec<&str> = users.iter()
    .filter(|u| u.active)
    .map(|u| u.name.as_str())
    .collect();
let pairs: Vec<(i32, i32)> = (0..3)
    .flat_map(|x| (0..3).map(move |y| (x, y)))
    .collect();
let flat: Vec<i32> = nested.iter()
    .flat_map(|sublist| sublist.iter().copied())
    .collect();
}

Словарное включение → collect в HashMap

# Python
word_lengths = {word: len(word) for word in words}
inverted = {v: k for k, v in mapping.items()}
#![allow(unused)]
fn main() {
// Rust
let word_lengths: HashMap<&str, usize> = words.iter()
    .map(|w| (*w, w.len()))
    .collect();
let inverted: HashMap<&V, &K> = mapping.iter()
    .map(|(k, v)| (v, k))
    .collect();
}

Включение множества → collect в HashSet

# Python
unique_lengths = {len(word) for word in words}
#![allow(unused)]
fn main() {
// Rust
let unique_lengths: HashSet<usize> = words.iter()
    .map(|w| w.len())
    .collect();
}

Частые методы итераторов

PythonRustПримечания
map(f, iter).map(f)Преобразует каждый элемент
filter(f, iter).filter(f)Оставляет подходящие элементы
sum(iter).sum()Суммирует все элементы
min(iter) / max(iter).min() / .max()Возвращает Option
any(f(x) for x in iter).any(f)True, если хотя бы один подходит
all(f(x) for x in iter).all(f)True, если подходят все
enumerate(iter).enumerate()Индекс и значение
zip(a, b)a.zip(b)Попарное объединение элементов
len(list).count() (потребляет итератор!) или .len()Подсчёт элементов
list(reversed(x)).rev()Обратный обход
itertools.chain(a, b)a.chain(b)Соединение итераторов
next(iter).next()Получить следующий элемент
next(iter, default).next().unwrap_or(default)С значением по умолчанию
list(iter).collect::<Vec<_>>()Материализация в коллекцию
sorted(iter)Собрать, затем .sort()Ленивого итератора сортировки нет
functools.reduce(f, iter).fold(init, f) или .reduce(f)Накопление

Ключевые различия

Итераторы Python:                     Итераторы Rust:
─────────────────                     ──────────────
- Ленивые по умолчанию (генераторы)   - Ленивые по умолчанию (все цепочки итераторов)
- yield создаёт генераторы            - impl Iterator { fn next() }
- StopIteration для завершения        - None для завершения
- Потребляются один раз               - Потребляются один раз
- Нет типовой безопасности            - Полностью типобезопасны
- Немного медленнее (интерпретатор)   - Без накладных расходов (компилируется)

Зачем нужны макросы в Rust

В Python нет системы макросов: для метапрограммирования используются декораторы, метаклассы и интроспекция во время выполнения. Rust использует макросы для генерации кода на этапе компиляции.

Метапрограммирование в Python и макросы Rust

# Python — декораторы и метаклассы для метапрограммирования
from dataclasses import dataclass
from functools import wraps

@dataclass              # Генерирует __init__, __repr__, __eq__ во время импорта
class Point:
    x: float
    y: float

# Собственный декоратор
def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def process(data):
    return data.upper()
#![allow(unused)]
fn main() {
// Rust — производные макросы и декларативные макросы для генерации кода
#[derive(Debug, Clone, PartialEq)]  // Генерирует реализации Debug, Clone, PartialEq на этапе КОМПИЛЯЦИИ
struct Point {
    x: f64,
    y: f64,
}

// Декларативный макрос (как шаблон)
macro_rules! log_call {
    ($func_name:expr, $body:expr) => {
        {
            println!("Вызов {}", $func_name);
            $body
        }
    };
}

fn process(data: &str) -> String {
    log_call!("process", data.to_uppercase())
}
}

Частые встроенные макросы

#![allow(unused)]
fn main() {
// Эти макросы используются повсюду в Rust:

println!("Hello, {}!", name);           // Вывод с форматированием
format!("Value: {}", x);               // Создание форматированной String
vec![1, 2, 3];                          // Создание Vec
assert_eq!(2 + 2, 4);                  // Проверка равенства в тесте
assert!(value > 0, "должно быть положительным"); // Проверка логического условия
dbg!(expression);                       // Отладочный вывод: печатает выражение И его значение
todo!();                                // Заглушка: компилируется, но паникует при вызове
unimplemented!();                       // Отметить код как нереализованный
panic!("something went wrong");         // Аварийное завершение с сообщением (как raise RuntimeError)

// Почему это макросы, а не функции?
// - println! принимает переменное число аргументов (функции Rust не могут)
// - vec! генерирует код для любого типа и размера
// - assert_eq! знает ИСХОДНЫЙ КОД того, что сравнивалось
// - dbg! знает ИМЯ ФАЙЛА и НОМЕР СТРОКИ
}

Пишем простой макрос с помощью macro_rules!

#![allow(unused)]
fn main() {
// Аналог dict() в Python
// Python: d = dict(a=1, b=2)
// Rust:   let d = hashmap!{ "a" => 1, "b" => 2 };

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = std::collections::HashMap::new();
            $(map.insert($key, $value);)*
            map
        }
    };
}

let scores = hashmap! {
    "Alice" => 100,
    "Bob" => 85,
    "Charlie" => 90,
};
}

Производные макросы: автоматическая реализация трейтов

#![allow(unused)]
fn main() {
// #[derive(...)] — это аналог декоратора @dataclass в Python

// Python:
// @dataclass(frozen=True, order=True)
// class Student:
//     name: str
//     grade: int

// Rust:
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Student {
    name: String,
    grade: i32,
}

// Частые производные макросы:
// Debug         → форматирование {:?} (как __repr__)
// Clone         → .clone(), глубокая копия
// Copy          → неявное копирование (только для простых типов)
// PartialEq, Eq → сравнение == (как __eq__)
// PartialOrd, Ord → <, >, сортировка (как __lt__ и др.)
// Hash          → можно использовать как ключ HashMap (как __hash__)
// Default       → MyType::default() (как __init__ без аргументов)

// Производные макросы из крейтов:
// Serialize, Deserialize (serde) → сериализация в JSON/YAML/TOML
//                                  (как json.dumps/loads в Python, но с проверкой типов)
}

Декоратор Python и производный макрос Rust

Декоратор PythonПроизводный макрос RustНазначение
@dataclass#[derive(Debug, Clone, PartialEq)]Класс данных
@dataclass(frozen=True)Неизменяемость по умолчаниюНеизменяемость
@dataclass(order=True)#[derive(Ord, PartialOrd)]Сравнение и сортировка
@total_ordering#[derive(PartialOrd, Ord)]Полный порядок
JSON json.dumps(obj.__dict__)#[derive(Serialize)]Сериализация
JSON MyClass(**json.loads(s))#[derive(Deserialize)]Десериализация

Упражнения

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

Задание: создайте структуру User с полями name: String, email: String и password_hash: String. Реализуйте Clone и PartialEq через derive, а Debug напишите вручную так, чтобы он выводил имя и email, а пароль скрывал (вместо него показывал "***").

🔑 Решение
use std::fmt;

#[derive(Clone, PartialEq)]
struct User {
    name: String,
    email: String,
    password_hash: String,
}

impl fmt::Debug for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("User")
            .field("name", &self.name)
            .field("email", &self.email)
            .field("password_hash", &"***")
            .finish()
    }
}

fn main() {
    let user = User {
        name: "Alice".into(),
        email: "alice@example.com".into(),
        password_hash: "a1b2c3d4e5f6".into(),
    };
    println!("{user:?}");
    // Output: User { name: "Alice", email: "alice@example.com", password_hash: "***" }
}

Ключевой вывод: в отличие от __repr__ в Python, Rust позволяет получить Debug через derive бесплатно, но его можно переопределить для чувствительных полей. Это безопаснее, чем в Python, где print(user) может случайно раскрыть секреты.


13. Конкурентность

Нет GIL: настоящий параллелизм

Что вы узнаете: почему GIL ограничивает конкурентность в Python, трейты Send/Sync Rust для проверки потокобезопасности на этапе компиляции, Arc<Mutex<T>> в сравнении с threading.Lock в Python, каналы в сравнении с queue.Queue и различия async/await.

Сложность: 🔴 Продвинутый

GIL (Global Interpreter Lock, глобальная блокировка интерпретатора) — главное ограничение Python для задач, ограниченных CPU. В Rust GIL нет: потоки действительно работают параллельно, а система типов предотвращает гонки данных на этапе компиляции.

gantt
    title Работа, ограниченная CPU: GIL в Python против потоков Rust
    dateFormat X
    axisFormat %s
    section Python (GIL)
        Поток 1 :a1, 0, 4
        Поток 2 :a2, 4, 8
        Поток 3 :a3, 8, 12
        Поток 4 :a4, 12, 16
    section Rust (без GIL)
        Поток 1 :b1, 0, 4
        Поток 2 :b2, 0, 4
        Поток 3 :b3, 0, 4
        Поток 4 :b4, 0, 4

Ключевая мысль: потоки Python при работе с CPU выполняются последовательно (GIL их сериализует). Потоки Rust действительно работают параллельно: 4 потока дают ускорение примерно в 4 раза.

📌 Предварительное требование: убедитесь, что вы освоили Гл. 7 — Владение и заимствование, прежде чем переходить к этой главе. Arc, Mutex и замыкания move опираются на понятия владения.

Проблема GIL в Python

# Python — потоки не помогают при работе, ограниченной CPU
import threading
import time

counter = 0

def increment(n):
    global counter
    for _ in range(n):
        counter += 1  # НЕ потокобезопасно! Но GIL как бы «защищает» простые операции

threads = [threading.Thread(target=increment, args=(1_000_000,)) for _ in range(4)]
start = time.perf_counter()
for t in threads:
    t.start()
for t in threads:
    t.join()
elapsed = time.perf_counter() - start

print(f"Счётчик: {counter}")    # Может быть не 4 000 000!
print(f"Время: {elapsed:.2f}s")  # Примерно ТАКОЕ ЖЕ время, как у одного потока (GIL)

# Для настоящего параллелизма в Python нужен multiprocessing:
from multiprocessing import Pool
with Pool(4) as pool:
    results = pool.map(cpu_work, data)  # Отдельные процессы, накладные расходы на pickle

Rust: настоящий параллелизм и проверка на этапе компиляции

use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicI64::new(0));

    let handles: Vec<_> = (0..4).map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            for _ in 0..1_000_000 {
                counter.fetch_add(1, Ordering::Relaxed);
            }
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("Счётчик: {}", counter.load(Ordering::Relaxed)); // Всегда 4 000 000
    // Работает на ВСЕХ ядрах: настоящий параллелизм, без GIL
}

Безопасность потоков: гарантии системы типов

Python: ошибки во время выполнения

# Python — гонки данных обнаруживаются во время выполнения (или не обнаруживаются вовсе)
import threading

shared_list = []

def append_items(items):
    for item in items:
        shared_list.append(item)  # «Потокобезопасно» благодаря GIL для append
        # Но сложные операции НЕ безопасны:
        # if item not in shared_list:
        #     shared_list.append(item)  # ГОНКА ДАННЫХ!

# Использование Lock для безопасности:
lock = threading.Lock()
def safe_append(items):
    for item in items:
        with lock:
            if item not in shared_list:
                shared_list.append(item)
# Забыли блокировку? Предупреждения компилятора не будет. Ошибка обнаружится в продакшене.

Rust: ошибки на этапе компиляции

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Попытка разделить Vec между потоками без защиты:
    // let shared = vec![];
    // thread::spawn(move || shared.push(1));
    // ❌ Ошибка компиляции: Vec не реализует Send/Sync без защиты

    // С Mutex (аналог threading.Lock в Rust):
    let shared = Arc::new(Mutex::new(Vec::new()));

    let handles: Vec<_> = (0..4).map(|i| {
        let shared = Arc::clone(&shared);
        thread::spawn(move || {
            let mut data = shared.lock().unwrap(); // Блокировка ОБЯЗАТЕЛЬНА для доступа
            data.push(i);
            // Блокировка автоматически освобождается, когда `data` выходит из области видимости
            // Нельзя «забыть разблокировать»: RAII гарантирует это
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("{:?}", shared.lock().unwrap()); // [0, 1, 2, 3] (порядок может отличаться)
}

Трейты Send и Sync

#![allow(unused)]
fn main() {
// Rust использует два маркерных трейта для обеспечения потокобезопасности:

// Send — «этот тип можно передать в другой поток»
// Большинство типов — Send. Rc<T> — НЕТ (для потоков используйте Arc<T>).

// Sync — «на этот тип можно ссылаться из нескольких потоков»
// Большинство типов — Sync. Cell<T>/RefCell<T> — НЕТ (используйте Mutex<T>).

// Компилятор проверяет это автоматически:
// thread::spawn(move || { ... })
//   ↑ Захваченные замыканием значения должны быть Send
//   ↑ Общие ссылки должны быть Sync
//   ↑ Если нет, то ошибка компиляции

// У Python аналога нет. Ошибки потокобезопасности обнаруживаются во время выполнения.
// Rust ловит их на этапе компиляции. Это и есть «конкурентность без страха».
}

Сравнение примитивов конкурентности

PythonRustНазначение
threading.Lock()Mutex<T>Взаимное исключение
threading.RLock()Mutex<T> (не реентерабельный)Реентерабельная блокировка (используется иначе)
threading.RWLock (нет)RwLock<T>Много читателей ИЛИ один писатель
threading.Event()CondvarУсловная переменная
queue.Queue()mpsc::channel()Потокобезопасный канал
multiprocessing.Poolrayon::ThreadPoolПул потоков
concurrent.futuresrayon / tokio::spawnПараллелизм на основе задач
threading.local()thread_local!Локальное для потока хранилище
НетТипы Atomic*Безблокировочные счётчики и флаги

Отравление мьютекса

Если поток паникует, удерживая Mutex, блокировка становится отравленной (poisoned). В Python аналога нет: если поток падает, удерживая threading.Lock(), блокировка остаётся захваченной навсегда.

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;

let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data2 = Arc::clone(&data);

let _ = thread::spawn(move || {
    let mut guard = data2.lock().unwrap();
    guard.push(4);
    panic!("упс!");  // Блокировка теперь отравлена
}).join();

// Последующие попытки блокировки возвращают Err(PoisonError)
match data.lock() {
    Ok(guard) => println!("Данные: {guard:?}"),
    Err(poisoned) => {
        println!("Блокировка отравлена! Восстанавливаем...");
        let guard = poisoned.into_inner();
        println!("Восстановлено: {guard:?}");  // [1, 2, 3, 4]
    }
}
}

Порядок атомарных операций (краткая заметка)

Параметр Ordering в атомарных операциях управляет гарантиями видимости памяти:

OrderingКогда использовать
RelaxedПростые счётчики, где порядок не важен
Acquire/ReleaseПроизводитель-потребитель: писатель использует Release, читатель Acquire
SeqCstЕсли сомневаетесь: самый строгий порядок, самый понятный

Модуль threading в Python скрывает эти детали за GIL. В Rust вы выбираете явно: используйте SeqCst, пока профилирование не покажет, что нужно что-то послабее.


Сравнение async/await

Python и Rust имеют синтаксис async/await, но внутри работают очень по-разному.

async/await в Python

# Python — asyncio для параллельного ввода-вывода
import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as resp:
        return await resp.text()

async def main():
    urls = ["https://example.com", "https://httpbin.org/get"]

    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

    for url, result in zip(urls, results):
        print(f"{url}: {len(result)} байт")

asyncio.run(main())

# Async в Python работает в одном потоке (всё ещё под GIL)!
# Помогает только при задачах, ограниченных вводом-выводом (ожидание сети или диска).
# Задачи, ограниченные CPU, внутри async всё равно блокируют цикл событий.

async/await в Rust

// Rust — tokio для параллельного ввода-вывода (и параллелизма CPU!)
use reqwest;
use tokio;
use futures::future::join_all;  // добавьте `futures` в Cargo.toml

async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
    reqwest::get(url).await?.text().await
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec!["https://example.com", "https://httpbin.org/get"];

    let tasks: Vec<_> = urls.iter()
        .map(|url| tokio::spawn(fetch_url(url)))  // Без ограничений GIL
        .collect();                                 // Можно использовать все ядра CPU

    let results = futures::future::join_all(tasks).await;

    for (url, result) in urls.iter().zip(results) {
        match result {
            Ok(Ok(body)) => println!("{url}: {} байт", body.len()),
            Ok(Err(e)) => println!("{url}: ошибка {e}"),
            Err(e) => println!("{url}: задача завершилась с ошибкой {e}"),
        }
    }

    Ok(())
}

Ключевые различия

АспектPython asyncioRust tokio
GILДействуетНет GIL
Параллелизм CPU❌ Один поток✅ Многопоточность
РантаймВстроен (asyncio)Внешний крейт (tokio)
Экосистемаaiohttp, asyncpg и др.reqwest, sqlx и др.
ПроизводительностьХорошо для I/OОтлично и для I/O, и для CPU
Обработка ошибокИсключенияResult<T, E>
Отменаtask.cancel()Удалить future (drop)
«Проблема цвета» функцийГраница sync ↔ asyncТа же проблема существует

Простой параллелизм с Rayon

# Python — multiprocessing для параллелизма CPU
from multiprocessing import Pool

def process_item(item):
    return heavy_computation(item)

with Pool(8) as pool:
    results = pool.map(process_item, items)
#![allow(unused)]
fn main() {
// Rust — rayon для лёгкого параллелизма CPU (изменение одной строки!)
use rayon::prelude::*;

// Последовательно:
let results: Vec<_> = items.iter().map(|item| heavy_computation(item)).collect();

// Параллельно (замените .iter() на .par_iter(), и всё!):
let results: Vec<_> = items.par_iter().map(|item| heavy_computation(item)).collect();

// Без pickle, без накладных расходов на процессы, без сериализации.
// Rayon автоматически распределяет работу по ядрам.
}

💼 Кейс: параллельный конвейер обработки изображений

Команда специалистов по данным ежедневно обрабатывает 50 000 спутниковых снимков. Их конвейер на Python использует multiprocessing.Pool:

# Python — multiprocessing для обработки изображений, ограниченной CPU
import multiprocessing
from PIL import Image
import numpy as np

def process_image(path: str) -> dict:
    img = np.array(Image.open(path))
    # Ресурсоёмкие операции: выравнивание гистограммы, выделение границ, классификация
    histogram = np.histogram(img, bins=256)[0]
    edges = detect_edges(img)       # ~200 мс на изображение
    label = classify(edges)          # ~100 мс на изображение
    return {"path": path, "label": label, "edge_count": len(edges)}

# Проблема: каждый подпроцесс копирует весь интерпретатор Python
# Память: 50 МБ на воркер × 16 воркеров = 800 МБ накладных расходов
# Запуск: 2–3 секунды на fork и pickle аргументов
with multiprocessing.Pool(16) as pool:
    results = pool.map(process_image, image_paths)  # ~4,5 часа на 50 тыс. изображений

Болевые точки: 800 МБ памяти на форк, сериализация pickle аргументов и результатов, GIL мешает использовать потоки, обработка ошибок непрозрачна (исключения в воркерах трудно отлаживать).

use rayon::prelude::*;
use image::GenericImageView;

struct ImageResult {
    path: String,
    label: String,
    edge_count: usize,
}

fn process_image(path: &str) -> Result<ImageResult, image::ImageError> {
    let img = image::open(path)?;
    // Специфичные для приложения функции (реализуйте под свою задачу)
    let histogram = compute_histogram(&img);       // ~50 мс (без накладных расходов numpy)
    let edges = detect_edges(&img);                // ~40 мс (оптимизировано через SIMD)
    let label = classify(&edges);                  // ~20 мс
    Ok(ImageResult {
        path: path.to_string(),
        label,
        edge_count: edges.len(),
    })
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let paths: Vec<String> = load_image_paths()?;

    // Rayon автоматически использует все ядра CPU: без форка, без pickle, без GIL
    let results: Vec<ImageResult> = paths
        .par_iter()                                // Параллельный итератор
        .filter_map(|p| process_image(p).ok())     // Аккуратно пропускаем ошибки
        .collect();                                // Собираем параллельно

    println!("Обработано изображений: {}", results.len());
    Ok(())
}
// 50 тыс. изображений за ~35 минут (против 4,5 часа в Python)
// Память: ~50 МБ всего (общие потоки, без форка)

Результаты:

МетрикаPython (multiprocessing)Rust (rayon)
Время (50 тыс. изображений)~4,5 часа~35 минут
Накладные расходы памяти800 МБ (16 воркеров)~50 МБ (общая)
Обработка ошибокНепрозрачные ошибки pickleResult<T, E> на каждом шаге
Стоимость запуска2–3 с (fork + pickle)Нет (потоки)

Ключевой урок: для параллельной работы, ограниченной CPU, потоки Rust и rayon заменяют multiprocessing из Python без накладных расходов на сериализацию, с общей памятью и проверкой безопасности на этапе компиляции.


Упражнения

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

Задание: в Python для защиты общего счётчика вы могли бы использовать threading.Lock. Перепишите это на Rust: запустите 10 потоков, каждый из которых 1000 раз увеличивает общий счётчик. Выведите итоговое значение (должно быть 10000). Используйте Arc<Mutex<u64>>.

🔑 Решение
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u64));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut num = counter.lock().unwrap();
                *num += 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Итоговый счёт: {}", *counter.lock().unwrap());
}

Ключевой вывод: Arc<Mutex<T>> — это аналог в Rust для lock = threading.Lock() плюс общая переменная, но Rust не скомпилируется, если вы забудете Arc или Mutex. Python спокойно запустит программу с гонкой и молча выдаст неверный ответ.


14. Unsafe Rust и FFI

Когда и зачем использовать unsafe

Что вы узнаете: что разрешает unsafe и почему он существует, написание расширений Python на PyO3 (главная возможность для разработчиков на Python), систему тестирования Rust в сравнении с pytest, моки с помощью mockall и бенчмаркинг.

Сложность: 🔴 Продвинутый

unsafe в Rust — это запасной люк: он сообщает компилятору «я делаю то, что ты не можешь проверить, но обещаю, что это корректно». У Python аналога нет, потому что Python никогда не даёт прямого доступа к памяти.

flowchart TB
    subgraph Safe ["Безопасный Rust (99% кода)"]
        S1["Логика вашего приложения"]
        S2["pub fn safe_api(&self) -> Result"]
    end
    subgraph Unsafe ["Блок unsafe (минимальный, проверенный)"]
        U1["Разыменование сырого указателя"]
        U2["Вызов FFI к C/Python"]
    end
    subgraph External ["Внешние (C / Python / ОС)"]
        E1["libc / PyO3 / системные вызовы"]
    end
    S1 --> S2
    S2 --> U1
    S2 --> U2
    U1 --> E1
    U2 --> E1
    style Safe fill:#d4edda,stroke:#28a745
    style Unsafe fill:#fff3cd,stroke:#ffc107
    style External fill:#f8d7da,stroke:#dc3545

Схема: безопасный API оборачивает небольшой блок unsafe. Вызывающий код никогда не видит unsafe. В ctypes Python такой границы нет: каждый вызов FFI неявно небезопасен.

📌 См. также: Гл. 13 — Конкурентность описывает трейты Send/Sync, которые являются небезопасными автотрейтами, и компилятор проверяет их для потокобезопасности.

Что разрешает unsafe

// unsafe позволяет делать ПЯТЬ вещей, которые запрещены в безопасном Rust:
// 1. Разыменовывать сырые указатели
// 2. Вызывать небезопасные функции и методы
// 3. Обращаться к изменяемым статическим переменным
// 4. Реализовывать небезопасные трейты
// 5. Обращаться к полям union

// Пример: вызов функции из C
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    // SAFETY: abs() — корректно определённая функция стандартной библиотеки C.
    let result = unsafe { abs(-42) };  // Безопасный Rust не может проверить код на C
    println!("{result}");               // 42
}

Когда использовать unsafe

#![allow(unused)]
fn main() {
// 1. FFI — вызов библиотек на C (самая частая причина)
// 2. Критичные к производительности внутренние циклы (редко)
// 3. Структуры данных, которые заимствующий checker не может выразить (редко)

// Как разработчик на Python, вы в основном встретите unsafe в:
// - внутренностях PyO3 (мост Python ↔ Rust)
// - привязках к библиотекам на C
// - низкоуровневых системных вызовах

// Практическое правило: если вы пишете прикладной код (а не библиотечный),
// unsafe почти никогда не нужен. Если кажется, что нужен, сначала спросите
// сообщество Rust: обычно есть безопасная альтернатива.
}

PyO3: расширения Rust для Python

PyO3 — мост между Python и Rust. Он позволяет писать функции и классы на Rust, которые вызываются из Python, и это идеально для замены медленных участков кода на Python.

Создание расширения Python на Rust

# Установка
pip install maturin    # Инструмент сборки расширений Rust для Python
maturin init           # Создаёт структуру проекта

# Структура проекта:
# my_extension/
# ├── Cargo.toml
# ├── pyproject.toml
# └── src/
#     └── lib.rs
# Cargo.toml
[package]
name = "my_extension"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]    # Разделяемая библиотека для Python

[dependencies]
pyo3 = { version = "0.22", features = ["extension-module"] }
#![allow(unused)]
fn main() {
// src/lib.rs — функции Rust, которые можно вызывать из Python
use pyo3::prelude::*;

/// Быстрая функция Фибоначчи, написанная на Rust.
#[pyfunction]
fn fibonacci(n: u64) -> u64 {
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 0..n {
        let temp = b;
        b = a.wrapping_add(b);
        a = temp;
    }
    a
}

/// Найти все простые числа до n (решето Эратосфена).
#[pyfunction]
fn primes_up_to(n: usize) -> Vec<usize> {
    let mut is_prime = vec![true; n + 1];
    is_prime[0] = false;
    if n > 0 { is_prime[1] = false; }
    for i in 2..=((n as f64).sqrt() as usize) {
        if is_prime[i] {
            for j in (i * i..=n).step_by(i) {
                is_prime[j] = false;
            }
        }
    }
    (2..=n).filter(|&i| is_prime[i]).collect()
}

/// Класс Rust, который можно использовать из Python.
#[pyclass]
struct Counter {
    value: i64,
}

#[pymethods]
impl Counter {
    #[new]
    fn new(start: i64) -> Self {
        Counter { value: start }
    }

    fn increment(&mut self) {
        self.value += 1;
    }

    fn get_value(&self) -> i64 {
        self.value
    }

    fn __repr__(&self) -> String {
        format!("Counter(value={})", self.value)
    }
}

/// Определение модуля Python.
#[pymodule]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
    m.add_function(wrap_pyfunction!(primes_up_to, m)?)?;
    m.add_class::<Counter>()?;
    Ok(())
}
}

Использование из Python

# Сборка и установка:
maturin develop --release   # Собирает и устанавливает в текущее venv
# Python — используйте расширение Rust как любой модуль Python
import my_extension

# Вызов функции на Rust
result = my_extension.fibonacci(50)
print(result)  # 12586269025 — вычислено за микросекунды

# Использование класса Rust
counter = my_extension.Counter(0)
counter.increment()
counter.increment()
print(counter.get_value())  # 2
print(counter)              # Counter(value=2)

# Сравнение производительности:
import time

# Версия на Python
def py_primes(n):
    sieve = [True] * (n + 1)
    for i in range(2, int(n**0.5) + 1):
        if sieve[i]:
            for j in range(i*i, n+1, i):
                sieve[j] = False
    return [i for i in range(2, n+1) if sieve[i]]

start = time.perf_counter()
py_result = py_primes(10_000_000)
py_time = time.perf_counter() - start

start = time.perf_counter()
rs_result = my_extension.primes_up_to(10_000_000)
rs_time = time.perf_counter() - start

print(f"Python: {py_time:.3f}s")    # ~3,5 с
print(f"Rust:   {rs_time:.3f}s")    # ~0,05 с — в 70 раз быстрее!
print(f"Результаты совпадают: {py_result == rs_result}")  # True

Краткая справка по PyO3

Концепция PythonАтрибут PyO3Примечания
Функция#[pyfunction]Доступна в Python
Класс#[pyclass]Класс, видимый в Python
Метод#[pymethods]Методы pyclass
__init__#[new]Конструктор
__repr__fn __repr__()Строковое представление
__str__fn __str__()Строка для вывода
__len__fn __len__()Длина
__getitem__fn __getitem__()Индексация
Property#[getter] / #[setter]Доступ к атрибутам
Static method#[staticmethod]Без self
Class method#[classmethod]Принимает cls

Шаблоны безопасности FFI

Когда Rust экспортируется в Python (через PyO3 или сырой C FFI), эти правила предотвращают самые частые ошибки:

  1. Никогда не пропускайте панику через границу FFI. Паника Rust, раскручивающаяся в Python (или C), — это неопределённое поведение. PyO3 делает это автоматически для #[pyfunction], но сырым функциям extern "C" нужна явная защита:

    #![allow(unused)]
    fn main() {
    #[no_mangle]
    pub extern "C" fn raw_ffi_function() -> i32 {
        match std::panic::catch_unwind(|| {
            // собственно логика
            42
        }) {
            Ok(result) => result,
            Err(_) => -1,  // Возвращаем код ошибки вместо паники в C/Python
        }
    }
    }
  2. #[repr(C)] для общих структур: если Python/C читает поля структуры напрямую, обязательно используйте #[repr(C)], чтобы гарантировать совместимую с C раскладку. Если передаются непрозрачные указатели (как делает PyO3 для #[pyclass]), это не нужно.

  3. extern "C": обязательно для сырых FFI-функций, чтобы соглашение о вызовах совпадало с тем, что ожидают C/Python. #[pyfunction] в PyO3 делает это за вас.

Преимущество PyO3: PyO3 берёт на себя большинство этих проблем безопасности: перехват паник, преобразование типов, управление GIL. Предпочитайте PyO3 сырому FFI, если нет особой причины не делать этого.


Модульные тесты и pytest

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

# test_calculator.py
import pytest
from calculator import add, divide

def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

def test_divide():
    assert divide(10, 2) == 5.0

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

# Параметризованные тесты
@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, -1, -2),
    (100, 200, 300),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

# Фикстуры
@pytest.fixture
def sample_data():
    return [1, 2, 3, 4, 5]

def test_sum(sample_data):
    assert sum(sample_data) == 15
# Запуск тестов
pytest                      # Запустить все тесты
pytest test_calculator.py   # Запустить один файл
pytest -k "test_add"        # Запустить подходящие тесты
pytest -v                   # Подробный вывод
pytest --tb=short           # Короткие трассировки

Встроенное тестирование в Rust

#![allow(unused)]
fn main() {
// src/calculator.rs — тесты находятся в ТОМ ЖЕ файле!
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Деление на ноль".to_string())
    } else {
        Ok(a / b)
    }
}

// Тесты размещаются в модуле #[cfg(test)]: он компилируется только при `cargo test`
#[cfg(test)]
mod tests {
    use super::*;  // Импортировать всё из родительского модуля

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_divide() {
        assert_eq!(divide(10.0, 2.0), Ok(5.0));
    }

    #[test]
    fn test_divide_by_zero() {
        assert!(divide(1.0, 0.0).is_err());
    }

    // Проверка, что что-то паникует (как pytest.raises)
    #[test]
    #[should_panic(expected = "out of bounds")]
    fn test_out_of_bounds() {
        let v = vec![1, 2, 3];
        let _ = v[99];  // Паника
    }
}
}
# Запуск тестов
cargo test                         # Запустить все тесты
cargo test test_add                # Запустить подходящие тесты
cargo test -- --nocapture          # Показать вывод println!
cargo test -p my_crate             # Тестировать один крейт в рабочем пространстве
cargo test -- --test-threads=1     # Последовательно (для тестов с побочными эффектами)

Краткая справка по тестированию

pytestRustПримечания
assert x == yassert_eq!(x, y)Равенство
assert x != yassert_ne!(x, y)Неравенство
assert conditionassert!(condition)Логическое условие
assert condition, "msg"assert!(condition, "msg")С сообщением
pytest.raises(E)#[should_panic]Ожидается паника
@pytest.fixtureНастройка в тесте или вспомогательной функцииВстроенных фикстур нет
@pytest.mark.parametrizeКрейт rstestПараметризованные тесты
conftest.pytests/common/mod.rsОбщие вспомогательные функции для тестов
pytest.skip()#[ignore]Пропустить тест
Фикстура tmp_pathКрейт tempfileВременные каталоги

Параметризованные тесты с rstest

#![allow(unused)]
fn main() {
// Cargo.toml: rstest = "0.23"

use rstest::rstest;

// Как @pytest.mark.parametrize
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, -1, -2)]
#[case(100, 200, 300)]
fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

// Как @pytest.fixture
use rstest::fixture;

#[fixture]
fn sample_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

#[rstest]
fn test_sum(sample_data: Vec<i32>) {
    assert_eq!(sample_data.iter().sum::<i32>(), 15);
}
}

Моки с mockall

# Python — моки с помощью unittest.mock
from unittest.mock import Mock, patch

def test_fetch_user():
    mock_db = Mock()
    mock_db.get_user.return_value = {"name": "Alice"}

    result = fetch_user_name(mock_db, 1)
    assert result == "Alice"
    mock_db.get_user.assert_called_once_with(1)
#![allow(unused)]
fn main() {
// Rust — моки с помощью крейта mockall
// Cargo.toml: mockall = "0.13"

use mockall::{automock, predicate::*};

#[automock]                          // Автоматически генерирует MockDatabase
trait Database {
    fn get_user(&self, id: i64) -> Option<User>;
}

fn fetch_user_name(db: &dyn Database, id: i64) -> Option<String> {
    db.get_user(id).map(|u| u.name)
}

#[test]
fn test_fetch_user() {
    let mut mock = MockDatabase::new();
    mock.expect_get_user()
        .with(eq(1))                   // assert_called_with(1)
        .times(1)                      // assert_called_once
        .returning(|_| Some(User { name: "Alice".into() }));

    let result = fetch_user_name(&mock, 1);
    assert_eq!(result, Some("Alice".to_string()));
}
}

Упражнения

🏋️ Упражнение: безопасная обёртка вокруг unsafe (нажмите, чтобы раскрыть)

Задание: напишите безопасную функцию split_at_mid, которая принимает &mut [i32] и возвращает два изменяемых среза (&mut [i32], &mut [i32]), разделённых посередине. Внутри используйте unsafe с сырыми указателями (имитируя то, что делает split_at_mut). Затем оберните это в безопасный API.

🔑 Решение
fn split_at_mid(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) {
    let mid = slice.len() / 2;
    let ptr = slice.as_mut_ptr();
    let len = slice.len();

    assert!(mid <= len); // Проверка безопасности до unsafe

    // SAFETY: mid <= len (проверено выше), а ptr получен из корректного &mut среза,
    // поэтому оба подсреза находятся в границах и не пересекаются.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut data = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mid(&mut data);
    left[0] = 99;
    right[0] = 88;
    println!("слева: {left:?}, справа: {right:?}");
    // слева: [99, 2, 3], справа: [88, 5, 6]
}

Ключевой вывод: блок unsafe небольшой и защищён assert!. Публичный API полностью безопасен: вызывающий код никогда не видит unsafe. Это паттерн Rust: небезопасные внутренности, безопасные интерфейсы. ctypes в Python не даёт таких гарантий.


15. Паттерны миграции

Частые паттерны 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 и критичный к производительности, с чистой границей. Не переписывайте клеящий код и обработчики, ограниченные вводом-выводом: выигрыш не окупит затрат.


16. Лучшие практики

Идиоматичный Rust для разработчиков на Python

Что вы узнаете: десять привычек, которые стоит выработать, частые ошибки и их исправления, структурированный трёхмесячный план обучения, полную справочную таблицу «Розеттский камень» Python → Rust и рекомендуемые ресурсы для изучения.

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

flowchart LR
    A["🟢 Недели 1–2<br/>Основы<br/>«Почему это не компилируется?»"] --> B["🟡 Недели 3–4<br/>Ключевые концепции<br/>«А, оно меня защищает»"]
    B --> C["🟡 Месяц 2<br/>Средний уровень<br/>«Теперь вижу, зачем это нужно»"]
    C --> D["🔴 Месяц 3+<br/>Продвинутый уровень<br/>«Поймал баг на этапе компиляции!»"]
    D --> E["🏆 Месяц 6<br/>Свободное владение<br/>«Стал лучшим программистом везде»"]
    style A fill:#d4edda
    style B fill:#fff3cd
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#c3e6cb,stroke:#28a745

Десять привычек, которые стоит выработать

  1. Используйте match для перечислений вместо if isinstance()

    # Python                              # Rust
    if isinstance(shape, Circle): ...     match shape { Shape::Circle(r) => ... }
    
  2. Доверяйте компилятору. Внимательно читайте сообщения об ошибках. Компилятор Rust — лучший среди всех языков. Он говорит, что не так, И как это исправить.

  3. Предпочитайте &str вместо String в параметрах функций. Принимайте наиболее общий тип. &str работает и с String, и с строковыми литералами.

  4. Используйте итераторы вместо циклов по индексу. Цепочки итераторов идиоматичнее и часто быстрее, чем for i in 0..vec.len().

  5. Используйте Option и Result в полной мере. Не вызывайте .unwrap() везде. Используйте ?, map, and_then, unwrap_or_else.

  6. Щедро используйте derive для трейтов. #[derive(Debug, Clone, PartialEq)] должен стоять на большинстве структур. Это бесплатно и упрощает тестирование.

  7. Регулярно запускайте cargo clippy. Он находит сотни проблем со стилем и корректностью. Относитесь к нему как к ruff для Rust.

  8. Не боритесь с borrow checker. Если вы с ним воюете, скорее всего, данные структурированы неправильно. Переделайте структуру, чтобы владение стало понятным.

  9. Используйте перечисления для конечных автоматов. Вместо строковых флагов или булевых значений используйте перечисления. Компилятор следит, чтобы обрабатывалось каждое состояние.

  10. Сначала клонируйте, потом оптимизируйте. На этапе изучения свободно используйте .clone(), чтобы не разбираться со сложностью владения. Оптимизируйте, только когда профилирование покажет необходимость.

Частые ошибки разработчиков на Python

ОшибкаПочему плохоИсправление
.unwrap() вездеПаника во время выполненияИспользуйте ? или match
String вместо &strЛишнее выделение памятиИспользуйте &str в параметрах
for i in 0..vec.len()Не идиоматичноfor item in &vec
Игнорирование предупреждений clippyУпускаете простые улучшенияcargo clippy
Слишком много вызовов .clone()Накладные расходыПересмотрите владение
Огромная функция main()Трудно тестироватьВынесите код в lib.rs
Не используете #[derive()]Изобретаете велосипедDerive для общих трейтов
Паника при ошибкахНе восстанавливаетсяВозвращайте Result<T, E>

Сравнение производительности

Бенчмарк: частые операции

Операция               Python 3.12    Rust (release)    Ускорение
─────────────────────  ────────────   ──────────────    ─────────
Фибоначчи(40)          ~25s           ~0.3s             ~80x
Сортировка 10 млн целых ~5.2s         ~0.6s             ~9x
Разбор JSON 100 МБ     ~8.5s          ~0.4s             ~21x
Регулярные выражения: 1 млн совпадений ~3.1s  ~0.3s    ~10x
HTTP-сервер (запросов/с) ~5,000       ~150,000          ~30x
SHA-256 для файла 1 ГБ ~12s           ~1.2s             ~10x
Разбор CSV: 1 млн строк ~4.5s         ~0.2s             ~22x
Конкатенация строк     ~2.1s          ~0.05s            ~42x

Примечание: Python с C-расширениями (NumPy и др.) заметно сокращает разрыв в численных расчётах. Эти бенчмарки сравнивают чистый Python с чистым Rust.

Использование памяти

Python:                                 Rust:
─────────                               ─────
- Заголовок объекта: 28 байт/объект    - Заголовка объекта нет
- int: 28 байт (даже для 0)            - i32: 4 байта, i64: 8 байт
- str "hello": 54 байта                - &str "hello": 16 байт (указатель + длина)
- список из 1000 int: ~36 КБ           - Vec<i32>: ~4 КБ
  (8 КБ указатели + 28 КБ объекты int)
- dict из 100 элементов: ~5,5 КБ       - HashMap из 100 элементов: ~2,4 КБ

Итого для типичного приложения:
- Python: базово 50–200 МБ             - Rust: базово 1–5 МБ

Частые ошибки и их решения

Ловушка 1: «Borrow checker не даёт»

#![allow(unused)]
fn main() {
// Проблема: попытка одновременно итерировать и менять коллекцию
let mut items = vec![1, 2, 3, 4, 5];
// for item in &items {
//     if *item > 3 { items.push(*item * 2); }  // ❌ Нельзя взять изменяемую ссылку, пока есть заимствование
// }

// Решение 1: собрать изменения и применить их после
let additions: Vec<i32> = items.iter()
    .filter(|&&x| x > 3)
    .map(|&x| x * 2)
    .collect();
items.extend(additions);

// Решение 2: используйте retain/extend
items.retain(|&x| x <= 3);
}

Ловушка 2: «Слишком много строковых типов»

#![allow(unused)]
fn main() {
// Если сомневаетесь:
// - &str для параметров функций
// - String для полей структур и возвращаемых значений
// - литералы &str ("hello") работают везде, где ожидается &str

fn process(input: &str) -> String {    // Принимаем &str, возвращаем String
    format!("Processed: {}", input)
}
}

Ловушка 3: «Мне не хватает простоты Python»

#![allow(unused)]
fn main() {
// Однострочник на Python:
// result = [x**2 for x in data if x > 0]

// Эквивалент на Rust:
let result: Vec<i32> = data.iter()
    .filter(|&&x| x > 0)
    .map(|&x| x * x)
    .collect();

// Он многословнее, зато:
// - типобезопасен на этапе компиляции
// - в 10–100 раз быстрее
// - ошибки типов во время выполнения невозможны
// - явно видно выделение памяти (.collect())
}

Ловушка 4: «А где мой REPL?»

#![allow(unused)]
fn main() {
// В Rust нет REPL. Вместо этого:
// 1. Используйте `cargo test` как REPL: пишите небольшие тесты, чтобы что-то проверить
// 2. Используйте Rust Playground (play.rust-lang.org) для быстрых экспериментов
// 3. Используйте макрос `dbg!()` для быстрого отладочного вывода
// 4. Используйте `cargo watch -x test`, чтобы тесты запускались при сохранении

#[test]
fn playground() {
    // Используйте это как свой «REPL»: запускайте через `cargo test playground`
    let result = "hello world"
        .split_whitespace()
        .map(|w| w.to_uppercase())
        .collect::<Vec<_>>();
    dbg!(&result);  // Выводит: [src/main.rs:5] &result = ["HELLO", "WORLD"]
}
}

Путь обучения и ресурсы

Недели 1–2: основы

  • Установите Rust и настройте VS Code с rust-analyzer
  • Пройдите главы 1–4 этого руководства (типы, управление потоком)
  • Напишите 5 небольших программ, переписав скрипты Python на Rust
  • Освойтесь с cargo build, cargo test, cargo clippy

Недели 3–4: ключевые концепции

  • Пройдите главы 5–8 (структуры, перечисления, владение, модули)
  • Перепишите на Rust скрипт обработки данных на Python
  • Практикуйтесь с Option<T> и Result<T, E>, пока это не станет естественным
  • Внимательно читайте сообщения компилятора: они вас учат

Месяц 2: средний уровень

  • Пройдите главы 9–12 (обработка ошибок, трейты, итераторы)
  • Создайте CLI-утилиту с clap и serde
  • Напишите расширение PyO3 для узкого места в проекте на Python
  • Практикуйтесь с цепочками итераторов, пока они не станут похожи на списочные включения

Месяц 3: продвинутый уровень

  • Пройдите главы 13–16 (конкурентность, unsafe, тестирование)
  • Создайте веб-сервис с axum и tokio
  • Внесите вклад в проект Rust с открытым исходным кодом
  • Прочитайте «Programming Rust» (O’Reilly), чтобы глубже понять язык

Рекомендуемые ресурсы

  • The Rust Book: https://doc.rust-lang.org/book/ (официальная книга, отличная)
  • Rust by Example: https://doc.rust-lang.org/rust-by-example/ (учитесь на практике)
  • Rustlings: https://github.com/rust-lang/rustlings (упражнения)
  • Rust Playground: https://play.rust-lang.org/ (онлайн-компилятор)
  • This Week in Rust: https://this-week-in-rust.org/ (рассылка)
  • PyO3 Guide: https://pyo3.rs/ (мост Python ↔ Rust)
  • Comprehensive Rust (Google): https://google.github.io/comprehensive-rust/

Шпаргалка Python → Rust

PythonRustГлава
listVec<T>5
dictHashMap<K,V>5
setHashSet<T>5
tuple(T1, T2, ...)5
classstruct + impl5
@dataclass#[derive(...)]5, 12a
Enumenum6
NoneOption<T>6
raise/try/exceptResult<T,E> + ?9
Protocol (PEP 544)trait10
TypeVarОбобщения <T>10
__dunder__ методыТрейты (Display, Add и др.)10
lambda|args| body12
генератор yieldimpl Iterator12
списочное включение.map().filter().collect()12
@decoratorФункция высшего порядка или макрос12a, 15
asynciotokio13
threadingstd::thread13
multiprocessingrayon13
unittest.mockmockall14a
pytestcargo test + rstest14a
pip installcargo add8
requirements.txtCargo.lock8
pyproject.tomlCargo.toml8
with (контекстный менеджер)Освобождение по области видимости (Drop)15
json.dumps/loadsserde_json15

Заключительные мысли для разработчиков на Python

#![allow(unused)]
fn main() {
Чего вам будет не хватать из Python:
- REPL и интерактивного исследования
- Скорости быстрого прототипирования
- Богатой экосистемы ML/AI (PyTorch и др.)
- Динамической типизации, которая «просто работает»
- pip install и мгновенного использования

Что вы получите от Rust:
- Уверенность «если компилируется, значит, работает»
- Прирост производительности в 10–100 раз
- Больше никаких ошибок типов во время выполнения
- Больше никаких падений из-за None/null
- Настоящий параллелизм (без GIL!)
- Развёртывание одним бинарником
- Предсказуемое потребление памяти
- Лучшие сообщения об ошибках компилятора среди всех языков

Путь:
Неделя 1:   «Почему компилятор меня ненавидит?»
Неделя 2:   «А, он на самом деле защищает меня от багов»
Месяц 1:    «Теперь вижу, зачем это нужно»
Месяц 2:    «Поймал баг на этапе компиляции, который стал бы инцидентом в продакшене»
Месяц 3:    «Не хочу возвращаться к коду без типов»
Месяц 6:    «Rust сделал меня лучшим программистом на любом языке»
}

Упражнения

🏋️ Упражнение: чек-лист код-ревью (нажмите, чтобы раскрыть)

Задание: проверьте этот код на Rust (его написал разработчик на Python) и найдите 5 идиоматичных улучшений:

fn get_name(names: Vec<String>, index: i32) -> String {
    if index >= 0 && (index as usize) < names.len() {
        return names[index as usize].clone();
    } else {
        return String::from("");
    }
}

fn main() {
    let mut result = String::from("");
    let names = vec!["Alice".to_string(), "Bob".to_string()];
    result = get_name(names.clone(), 0);
    println!("{}", result);
}
🔑 Решение

Пять улучшений:

// 1. Принимайте &[String], а не Vec<String> (не забирайте владение всем вектором)
// 2. Используйте usize для индекса (не i32: индексы всегда неотрицательны)
// 3. Возвращайте Option<&str> вместо пустой строки (используйте систему типов!)
// 4. Используйте .get() вместо ручной проверки границ
// 5. Не вызывайте clone() в main: передавайте ссылку

fn get_name(names: &[String], index: usize) -> Option<&str> {
    names.get(index).map(|s| s.as_str())
}

fn main() {
    let names = vec!["Alice".to_string(), "Bob".to_string()];
    match get_name(&names, 0) {
        Some(name) => println!("{name}"),
        None => println!("Не найдено"),
    }
}

Ключевой вывод: привычки Python, которые мешают в Rust: клонирование всего (используйте заимствования), значения-заглушки вроде "" (используйте Option), забирание владения там, где достаточно заимствования, и знаковые целые для индексов.


Конец учебного руководства «Rust для программистов на Python»

17. Итоговый проект: CLI-менеджер задач

Итоговый проект: CLI-менеджер задач

Что вы узнаете: как объединить всё изученное в курсе, написав полноценное CLI-приложение на Rust. Обычно такое приложение пишут на Python с помощью argparse + json + pathlib.

Сложность: 🔴 Продвинутый

Этот итоговый проект использует концепции из каждой ключевой главы:

  • Гл. 3: типы и переменные (структуры, перечисления)
  • Гл. 5: коллекции (Vec, HashMap)
  • Гл. 6: перечисления и сопоставление с образцом (статус задачи, команды)
  • Гл. 7: владение и заимствование (передача ссылок)
  • Гл. 9: обработка ошибок (Result, ?, собственные ошибки)
  • Гл. 10: трейты (Display, FromStr)
  • Гл. 11: преобразования типов (From, TryFrom)
  • Гл. 12: итераторы и замыкания (фильтрация, преобразование)
  • Гл. 8: модули (организация структуры проекта)

Проект: rustdo

Менеджер задач для командной строки (вроде инструментов todo.txt в Python), который хранит задачи в JSON-файле.

Аналог на Python (что вы написали бы на Python)

#!/usr/bin/env python3
"""Простой CLI-менеджер задач: версия на Python."""
import json
import sys
from pathlib import Path
from datetime import datetime
from enum import Enum

TASK_FILE = Path.home() / ".rustdo.json"

class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class Task:
    def __init__(self, id: int, title: str, priority: Priority, done: bool = False):
        self.id = id
        self.title = title
        self.priority = priority
        self.done = done
        self.created = datetime.now().isoformat()

def load_tasks() -> list[Task]:
    if not TASK_FILE.exists():
        return []
    data = json.loads(TASK_FILE.read_text())
    return [Task(**t) for t in data]

def save_tasks(tasks: list[Task]):
    TASK_FILE.write_text(json.dumps([t.__dict__ for t in tasks], indent=2))

# Команды: add, list, done, remove, stats
# ... (вы и так знаете, как это делается на Python)

Ваша реализация на Rust

Собирайте проект по шагам. Каждый шаг соответствует концепциям из определённых глав.


Шаг 1: определите модель данных (гл. 3, 6, 10, 11)

#![allow(unused)]
fn main() {
// src/task.rs
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use chrono::Local;

/// Приоритет задачи: соответствует Priority(Enum) в Python
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
    Low,
    Medium,
    High,
}

// Трейт Display (аналог __str__ в Python)
impl fmt::Display for Priority {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Priority::Low => write!(f, "low"),
            Priority::Medium => write!(f, "medium"),
            Priority::High => write!(f, "high"),
        }
    }
}

// Трейт FromStr (разбор "high" → Priority::High)
impl FromStr for Priority {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "low" | "l" => Ok(Priority::Low),
            "medium" | "med" | "m" => Ok(Priority::Medium),
            "high" | "h" => Ok(Priority::High),
            other => Err(format!("неизвестный приоритет: '{other}' (используйте low/medium/high)")),
        }
    }
}

/// Одна задача: соответствует классу Task в Python
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub title: String,
    pub priority: Priority,
    pub done: bool,
    pub created: String,
}

impl Task {
    pub fn new(id: u32, title: String, priority: Priority) -> Self {
        Self {
            id,
            title,
            priority,
            done: false,
            created: Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
        }
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let status = if self.done { "✅" } else { "⬜" };
        let priority_icon = match self.priority {
            Priority::Low => "🟢",
            Priority::Medium => "🟡",
            Priority::High => "🔴",
        };
        write!(f, "{} {} [{}] {} ({})", status, self.id, priority_icon, self.title, self.created)
    }
}
}

Сравнение с Python: в Python вы бы использовали @dataclass + Enum. В Rust struct + enum + макросы derive дают сериализацию, вывод и разбор бесплатно.


Шаг 2: слой хранения (гл. 9, 7)

#![allow(unused)]
fn main() {
// src/storage.rs
use std::fs;
use std::path::PathBuf;
use crate::task::Task;

/// Путь к файлу задач (~/.rustdo.json)
fn task_file_path() -> PathBuf {
    let home = dirs::home_dir().expect("Не удалось определить домашний каталог");
    home.join(".rustdo.json")
}

/// Загрузить задачи с диска: возвращает пустой Vec, если файла нет
pub fn load_tasks() -> Result<Vec<Task>, Box<dyn std::error::Error>> {
    let path = task_file_path();
    if !path.exists() {
        return Ok(Vec::new());
    }
    let content = fs::read_to_string(&path)?;  // ? передаёт io::Error дальше
    let tasks: Vec<Task> = serde_json::from_str(&content)?;  // ? передаёт ошибку serde дальше
    Ok(tasks)
}

/// Сохранить задачи на диск
pub fn save_tasks(tasks: &[Task]) -> Result<(), Box<dyn std::error::Error>> {
    let path = task_file_path();
    let json = serde_json::to_string_pretty(tasks)?;
    fs::write(&path, json)?;
    Ok(())
}
}

Сравнение с Python: Python использует Path.read_text() + json.loads(). Rust использует fs::read_to_string() + serde_json::from_str(). Обратите внимание на ?: каждая ошибка явная и передаётся дальше.


Шаг 3: перечисление команд (гл. 6)

#![allow(unused)]
fn main() {
// src/command.rs
use crate::task::Priority;

/// Все возможные команды: один вариант перечисления на каждое действие
pub enum Command {
    Add { title: String, priority: Priority },
    List { show_done: bool },
    Done { id: u32 },
    Remove { id: u32 },
    Stats,
    Help,
}

impl Command {
    /// Разбирает аргументы командной строки в Command
    /// (в реальном проекте используйте `clap`, а здесь это учебный пример)
    pub fn parse(args: &[String]) -> Result<Self, String> {
        match args.first().map(|s| s.as_str()) {
            Some("add") => {
                let title = args.get(1)
                    .ok_or("использование: rustdo add <title> [priority]")?
                    .clone();
                let priority = args.get(2)
                    .map(|p| p.parse::<Priority>())
                    .transpose()
                    .map_err(|e| e.to_string())?
                    .unwrap_or(Priority::Medium);
                Ok(Command::Add { title, priority })
            }
            Some("list") => {
                let show_done = args.get(1).map(|s| s == "--all").unwrap_or(false);
                Ok(Command::List { show_done })
            }
            Some("done") => {
                let id: u32 = args.get(1)
                    .ok_or("использование: rustdo done <id>")?
                    .parse()
                    .map_err(|_| "id должен быть числом")?;
                Ok(Command::Done { id })
            }
            Some("remove") => {
                let id: u32 = args.get(1)
                    .ok_or("использование: rustdo remove <id>")?
                    .parse()
                    .map_err(|_| "id должен быть числом")?;
                Ok(Command::Remove { id })
            }
            Some("stats") => Ok(Command::Stats),
            _ => Ok(Command::Help),
        }
    }
}
}

Сравнение с Python: Python использует argparse или click. Этот самописный разбор показывает, как match по шаблонам, похожим на перечисления, заменяет цепочки if/elif в Python. Для реальных проектов используйте крейт clap.


Шаг 4: бизнес-логика (гл. 5, 12, 7)

#![allow(unused)]
fn main() {
// src/actions.rs
use crate::task::{Task, Priority};
use crate::storage;

pub fn add_task(title: String, priority: Priority) -> Result<(), Box<dyn std::error::Error>> {
    let mut tasks = storage::load_tasks()?;
    let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
    let task = Task::new(next_id, title.clone(), priority);
    println!("Добавлено: {task}");
    tasks.push(task);
    storage::save_tasks(&tasks)?;
    Ok(())
}

pub fn list_tasks(show_done: bool) -> Result<(), Box<dyn std::error::Error>> {
    let tasks = storage::load_tasks()?;
    let filtered: Vec<&Task> = tasks.iter()
        .filter(|t| show_done || !t.done)   // Итератор + замыкание (гл. 12)
        .collect();

    if filtered.is_empty() {
        println!("Задач нет! 🎉");
        return Ok(());
    }

    for task in &filtered {
        println!("  {task}");   // Использует трейт Display (гл. 10)
    }
    println!("\nПоказано задач: {}", filtered.len());
    Ok(())
}

pub fn complete_task(id: u32) -> Result<(), Box<dyn std::error::Error>> {
    let mut tasks = storage::load_tasks()?;
    let task = tasks.iter_mut()
        .find(|t| t.id == id)                // Iterator::find (гл. 12)
        .ok_or(format!("Задачи с id {id} нет"))?;
    task.done = true;
    println!("Выполнено: {task}");
    storage::save_tasks(&tasks)?;
    Ok(())
}

pub fn remove_task(id: u32) -> Result<(), Box<dyn std::error::Error>> {
    let mut tasks = storage::load_tasks()?;
    let len_before = tasks.len();
    tasks.retain(|t| t.id != id);            // Vec::retain (гл. 5)
    if tasks.len() == len_before {
        return Err(format!("Задачи с id {id} нет").into());
    }
    println!("Удалена задача {id}");
    storage::save_tasks(&tasks)?;
    Ok(())
}

pub fn show_stats() -> Result<(), Box<dyn std::error::Error>> {
    let tasks = storage::load_tasks()?;
    let total = tasks.len();
    let done = tasks.iter().filter(|t| t.done).count();
    let pending = total - done;

    // Группировка по приоритету с помощью итераторов (гл. 12)
    let high = tasks.iter().filter(|t| !t.done && t.priority == Priority::High).count();
    let medium = tasks.iter().filter(|t| !t.done && t.priority == Priority::Medium).count();
    let low = tasks.iter().filter(|t| !t.done && t.priority == Priority::Low).count();

    println!("📊 Статистика задач");
    println!("   Всего:       {total}");
    println!("   Выполнено:   {done} ✅");
    println!("   В работе:    {pending}");
    println!("   🔴 Высокий:  {high}");
    println!("   🟡 Средний:  {medium}");
    println!("   🟢 Низкий:   {low}");
    Ok(())
}
}

Используемые паттерны Rust: iter().map().max(), iter().filter().collect(), iter_mut().find(), retain(), iter().filter().count(). Они заменяют списочные включения Python, next(x for x in ...) и Counter.


Шаг 5: собираем всё вместе (гл. 8)

// src/main.rs
mod task;
mod storage;
mod command;
mod actions;

use command::Command;

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let command = match Command::parse(&args) {
        Ok(cmd) => cmd,
        Err(e) => {
            eprintln!("Ошибка: {e}");
            std::process::exit(1);
        }
    };

    let result = match command {
        Command::Add { title, priority } => actions::add_task(title, priority),
        Command::List { show_done } => actions::list_tasks(show_done),
        Command::Done { id } => actions::complete_task(id),
        Command::Remove { id } => actions::remove_task(id),
        Command::Stats => actions::show_stats(),
        Command::Help => {
            print_help();
            Ok(())
        }
    };

    if let Err(e) = result {
        eprintln!("Ошибка: {e}");
        std::process::exit(1);
    }
}

fn print_help() {
    println!("rustdo — менеджер задач для питонистов, которые изучают Rust\n");
    println!("ИСПОЛЬЗОВАНИЕ:");
    println!("  rustdo add <title> [low|medium|high]   Добавить задачу");
    println!("  rustdo list [--all]                    Показать невыполненные задачи");
    println!("  rustdo done <id>                       Отметить задачу выполненной");
    println!("  rustdo remove <id>                     Удалить задачу");
    println!("  rustdo stats                           Показать статистику");
}
graph TD
    CLI["main.rs<br/>(точка входа CLI)"] --> CMD["command.rs<br/>(разбор аргументов)"]
    CMD --> ACT["actions.rs<br/>(бизнес-логика)"]
    ACT --> STORE["storage.rs<br/>(хранение в JSON)"]
    ACT --> TASK["task.rs<br/>(модель данных)"]
    STORE --> TASK
    style CLI fill:#d4edda
    style CMD fill:#fff3cd
    style ACT fill:#fff3cd
    style STORE fill:#ffeeba
    style TASK fill:#ffeeba

Шаг 6: зависимости в Cargo.toml

[package]
name = "rustdo"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4"
dirs = "5"

Аналог в Python: это ваш [project.dependencies] из pyproject.toml. cargo add serde serde_json chrono dirs — аналог pip install.


Шаг 7: тесты (гл. 14)

#![allow(unused)]
fn main() {
// src/task.rs — добавьте в конец файла
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_priority() {
        assert_eq!("high".parse::<Priority>().unwrap(), Priority::High);
        assert_eq!("H".parse::<Priority>().unwrap(), Priority::High);
        assert_eq!("med".parse::<Priority>().unwrap(), Priority::Medium);
        assert!("invalid".parse::<Priority>().is_err());
    }

    #[test]
    fn task_display() {
        let task = Task::new(1, "Write Rust".to_string(), Priority::High);
        let display = format!("{task}");
        assert!(display.contains("Write Rust"));
        assert!(display.contains("🔴"));
        assert!(display.contains("⬜")); // Ещё не выполнена
    }

    #[test]
    fn task_serialization_roundtrip() {
        let task = Task::new(1, "Test".to_string(), Priority::Low);
        let json = serde_json::to_string(&task).unwrap();
        let recovered: Task = serde_json::from_str(&json).unwrap();
        assert_eq!(recovered.title, "Test");
        assert_eq!(recovered.priority, Priority::Low);
    }
}
}

Аналог в Python: тесты pytest. Запускайте их через cargo test вместо pytest. Никакой магии поиска тестов не нужно: #[test] явно помечает тестовые функции.


Дополнительные задания

Когда базовая версия заработает, попробуйте эти улучшения:

  1. Добавьте clap для разбора аргументов: замените самописный разбор макросами derive из clap:

    #![allow(unused)]
    fn main() {
    #[derive(Parser)]
    enum Command {
        Add { title: String, #[arg(default_value = "medium")] priority: Priority },
        List { #[arg(long)] all: bool },
        Done { id: u32 },
        Remove { id: u32 },
        Stats,
    }
    }
  2. Добавьте цветной вывод: используйте крейт colored для цветов в терминале (аналог colorama в Python).

  3. Добавьте сроки выполнения: добавьте поле Option<NaiveDate> и фильтруйте просроченные задачи.

  4. Добавьте теги и категории: используйте Vec<String> для тегов и фильтруйте через .iter().any().

  5. Сделайте библиотеку и исполняемый файл: разделите код на lib.rs и main.rs, чтобы логику можно было переиспользовать (паттерн модулей, гл. 8).


Что вы попрактиковали

ГлаваКонцепцияГде использовалась
Гл. 3Типы и переменныеПоля структуры Task, u32, String, bool
Гл. 5КоллекцииVec<Task>, retain(), push()
Гл. 6Перечисления и matchPriority, Command, исчерпывающее сопоставление
Гл. 7Владение и заимствование&[Task] и Vec<Task>, &mut для отметки о выполнении
Гл. 8Модулиmod task; mod storage; mod command; mod actions;
Гл. 9Обработка ошибокResult<T, E>, оператор ?, .ok_or()
Гл. 10ТрейтыDisplay, FromStr, Serialize, Deserialize
Гл. 11From/IntoFromStr для Priority, .into() для преобразования ошибок
Гл. 12Итераторыfilter, map, find, count, collect
Гл. 14Тестирование#[test], #[cfg(test)], макросы проверок

🎓 Поздравляем! Если вы создали этот проект, вы использовали каждую ключевую концепцию Rust, которая разобрана в этой книге. Вы больше не программист на Python, который учит Rust, а разработчик на Rust, который знает Python.