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>> |
| 14 | Unsafe, 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. Введение и мотивация 🟢
- Аргументы в пользу Rust для разработчиков на Python
- Частые проблемы Python, которые решает Rust
- Когда выбирать Rust вместо Python
2. Начало работы 🟢
3. Встроенные типы и переменные 🟢
4. Управление потоком выполнения 🟢
5. Структуры данных и коллекции 🟢
6. Перечисления и сопоставление с образцом 🟡
- Алгебраические типы данных и объединения типов
- Исчерпывающее сопоставление с образцом
- Option для безопасной работы с None
Часть II — Ключевые концепции
7. Владение и заимствование 🟡
- Понимание владения
- Семантика перемещения и подсчёт ссылок
- Заимствование и времена жизни
- Умные указатели
8. Крейты и модули 🟢
9. Обработка ошибок 🟡
10. Трейты и обобщения 🟡
11. Трейты From и Into 🟡
12. Замыкания и итераторы 🟡
Часть III — Продвинутые темы и миграция
13. Конкурентность 🔴
14. Unsafe Rust, FFI и тестирование 🔴
15. Паттерны миграции 🟡
- Частые паттерны Python в Rust
- Основные крейты для разработчиков на Python
- Стратегия постепенного внедрения
16. Лучшие практики 🟡
- Идиоматичный Rust для разработчиков на Python
- Частые ошибки и их решения
- Шпаргалка «Python → Rust»
- Путь обучения и ресурсы
Часть 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напрямую в несколько инструкций x86add/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:
- Взаимодополняющие навыки: Rust и Python решают разные задачи
- Мост PyO3: пишите расширения на Rust, которые можно вызывать из Python
- Понимание производительности: вы узнаете, почему Python медленный и как исправлять узкие места
- Карьерный рост: экспертиза в системном программировании становится всё ценнее
- Облачные расходы: код в 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
| Концепция | Python | Rust | Ключевое отличие |
|---|---|---|---|
| Типизация | Динамическая (duck typing) | Статическая (на этапе компиляции) | Ошибки обнаруживаются до выполнения |
| Память | Сборка мусора (подсчёт ссылок + GC для циклов) | Система владения | Без накладных расходов, детерминированная очистка |
| None/null | None где угодно | Option<T> | Безопасность None на этапе компиляции |
| Обработка ошибок | raise/try/except | Result<T, E> | Явно, без скрытого потока управления |
| Изменяемость | Всё изменяемо | Неизменяемость по умолчанию | Изменение нужно включать явно |
| Скорость | Интерпретируемый (~в 10–100 раз медленнее) | Компилируемый (скорость C/C++) | На порядки быстрее |
| Конкурентность | GIL ограничивает потоки | Нет GIL, трейты Send/Sync | Настоящий параллелизм по умолчанию |
| Зависимости | pip install / poetry add | cargo add | Управление зависимостями встроено |
| Система сборки | setuptools/poetry/hatch | Cargo | Один единый инструмент |
| Упаковка | pyproject.toml | Cargo.toml | Похожая декларативная конфигурация |
| REPL | Интерактивный python | Нет REPL (используйте тесты или cargo run) | Процесс, ориентированный на компиляцию |
| Подсказки типов | Необязательные, не проверяются | Обязательные, проверяются компилятором | Типы — не украшение |
Упражнения
🏋️ Упражнение: проверка ментальной модели (нажмите, чтобы раскрыть)
Задание: для каждого фрагмента на Python предскажите, что в Rust потребуется иначе. Код писать не нужно, просто опишите ограничение.
x = [1, 2, 3]; y = x; x.append(4)— что произойдёт в Rust?data = None; print(data.upper())— как Rust этому помешает?import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start()— что требует Rust?
🔑 Решение
- Перемещение владения:
let y = x;перемещаетx, поэтомуx.push(4)— ошибка компиляции. Понадобилось быlet y = x.clone();или заимствование черезlet y = &x;. - Нет null:
dataне может бытьNone, если только это неOption<String>. Нужно использоватьmatch,.unwrap()илиif let, и неожиданных ошибокNoneTypeне будет. - 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
| Назначение | Python | Rust |
|---|---|---|
| Среда выполнения языка | python (интерпретатор) | rustc (компилятор, напрямую вызывается редко) |
| Менеджер пакетов | pip / poetry / uv | cargo (встроен) |
| Конфигурация проекта | pyproject.toml | Cargo.toml |
| Lock-файл | poetry.lock / requirements.txt | Cargo.lock |
| Виртуальное окружение | venv / conda | Не нужно (зависимости хранятся для каждого проекта) |
| Форматтер | black / ruff format | rustfmt (встроен: cargo fmt) |
| Линтер | ruff / flake8 / pylint | clippy (встроен: cargo clippy) |
| Проверка типов | mypy / pyright | Встроена в компилятор (всегда включена) |
| Запуск тестов | pytest | cargo test (встроен) |
| Документация | sphinx / mkdocs | cargo doc (встроен) |
| REPL | python / 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 и напишите программу, которая:
- Объявляет переменную
nameс вашим именем (тип&str) - Объявляет изменяемую переменную
count, начальное значение 0 - Использует цикл
forот 1..=5, чтобы увеличиватьcountи выводить"Привет, {name}! (счётчик: {count})" - После цикла выводит, чётное или нечётное значение
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
Числовые типы
| Python | Rust | Примечания |
|---|---|---|
int (произвольная точность) | i8, i16, i32, i64, i128, isize | Целые числа в Rust имеют фиксированный размер |
int (беззнаковые: отдельного типа нет) | u8, u16, u32, u64, u128, usize | Беззнаковые типы указываются явно |
float (64-бит, IEEE 754) | f32, f64 | В Python есть только 64-битный float |
bool | bool | Та же концепция |
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:?}"); // То же самое, но со встроенным синтаксисом
}
Краткая справка
| Python | Rust | Примечания |
|---|---|---|
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 в куче. Структура RustRectangleзанимает ровно 16 байт на стеке: без косвенных обращений и без нагрузки на GC.📌 См. также: Гл. 10 — Трейты и обобщения рассказывает о реализации трейтов вроде
Display,Debugи перегрузки операторов для ваших структур.
Соответствие: магические методы Python → трейты Rust
| Python | Rust | Назначение |
|---|---|---|
__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
}
Краткая справка
| Python | Rust | Примечания |
|---|---|---|
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 lst | vec.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;
}
}
Краткая справка
| Python | Rust | Примечания |
|---|---|---|
d[key] = val | d.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 d | d.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 |
Другие коллекции
| Python | Rust | Примечания |
|---|---|---|
set() | HashSet<T> | use std::collections::HashSet; |
collections.deque | VecDeque<T> | use std::collections::VecDeque; |
heapq | BinaryHeap<T> | Max-куча по умолчанию |
collections.OrderedDict | IndexMap (крейт) | HashMap не сохраняет порядок |
sortedcontainers.SortedList | BTreeSet<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
| Задача | Python | Rust |
|---|---|---|
| Проверка наличия | if x is not None: | if let Some(x) = opt { |
| Значение по умолчанию | x or default | opt.unwrap_or(default) |
| Фабрика значения по умолчанию | x or compute() | opt.unwrap_or_else(|| compute()) |
| Преобразование, если значение есть | f(x) if x else None | opt.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 raise | opt.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}!")
}
Исправленные ошибки:
- Неизменяемое заимствование и изменение:
firstзаимствуетnames, а затемpushего изменяет. Исправление: использоватьfirstдоpush. - Перемещение из Vec:
names[0]пытается переместить String из Vec, а это запрещено. Исправление: заимствовать через&names[0]. - Функция забирает владение:
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вашего верхнеуровневого пакета.
Ключевые различия
| Понятие | Python | Rust |
|---|---|---|
| Модуль = файл | ✅ Автоматически | Нужно объявить через mod |
| Пакет = каталог | __init__.py | mod.rs |
| Публично по умолчанию | ✅ Всё | ❌ По умолчанию приватно |
| Сделать публичным | Соглашение с префиксом _ | Ключевое слово pub |
| Синтаксис импорта | from x import y | use x::y; |
| Импорт всего | from x import * | use x::*; (не рекомендуется) |
| Относительный импорт | from . import sibling | use 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 | Назначение |
|---|---|---|
requests | reqwest | HTTP-клиент |
json (stdlib) | serde_json | Разбор JSON |
pydantic | serde | Сериализация и валидация |
pathlib | std::path (stdlib) | Работа с путями |
os / shutil | std::fs (stdlib) | Операции с файлами |
re | regex | Регулярные выражения |
logging | tracing / log | Логирование |
click / argparse | clap | Разбор аргументов CLI |
asyncio | tokio | Асинхронный рантайм |
datetime | chrono | Дата и время |
pytest | Встроенное + rstest | Тестирование |
dataclasses | #[derive(...)] | Структуры данных |
typing.Protocol | Трейты | Структурная типизация |
subprocess | std::process (stdlib) | Запуск внешних команд |
sqlite3 | rusqlite | SQLite |
sqlalchemy | diesel / sqlx | ORM / инструментарий SQL |
fastapi | axum / 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)
}
}
Краткая справка по обработке ошибок
| Python | Rust | Примечания |
|---|---|---|
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>, которая:
- Отклоняет пустые строки с ошибкой
"пустой ввод" - Разбирает строку в
u16, преобразуя ошибку разбора в"некорректное число: {исходная_ошибка}" - Отклоняет порты ниже 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);
}
}
Краткая справка по обобщениям
| Python | Rust | Примечания |
|---|---|---|
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) |
Clone | copy.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<A> for B"] -->|"генерирует автоматически"| B["impl Into<B> 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 бесплатно.
}
Краткая справка по преобразованиям
| Python | Rust | Примечания |
|---|---|---|
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.
Упражнения
🏋️ Упражнение: библиотека преобразования температур (нажмите, чтобы раскрыть)
Задание: создайте мини-библиотеку преобразования температур:
- Определите структуры
Celsius(f64),Fahrenheit(f64)иKelvin(f64) - Реализуйте
From<Celsius> for FahrenheitиFrom<Celsius> for Kelvin - Реализуйте
TryFrom<f64> for Kelvin, который отклоняет значения ниже абсолютного нуля (-273.15°C = 0K) - Реализуйте
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<i32><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();
}
Частые методы итераторов
| Python | Rust | Примечания |
|---|---|---|
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/SyncRust для проверки потокобезопасности на этапе компиляции,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 ловит их на этапе компиляции. Это и есть «конкурентность без страха».
}
Сравнение примитивов конкурентности
| Python | Rust | Назначение |
|---|---|---|
threading.Lock() | Mutex<T> | Взаимное исключение |
threading.RLock() | Mutex<T> (не реентерабельный) | Реентерабельная блокировка (используется иначе) |
threading.RWLock (нет) | RwLock<T> | Много читателей ИЛИ один писатель |
threading.Event() | Condvar | Условная переменная |
queue.Queue() | mpsc::channel() | Потокобезопасный канал |
multiprocessing.Pool | rayon::ThreadPool | Пул потоков |
concurrent.futures | rayon / 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 asyncio | Rust 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 МБ (общая) |
| Обработка ошибок | Непрозрачные ошибки pickle | Result<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. ВctypesPython такой границы нет: каждый вызов 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), эти правила предотвращают самые частые ошибки:
-
Никогда не пропускайте панику через границу 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 } } } -
#[repr(C)]для общих структур: если Python/C читает поля структуры напрямую, обязательно используйте#[repr(C)], чтобы гарантировать совместимую с C раскладку. Если передаются непрозрачные указатели (как делает PyO3 для#[pyclass]), это не нужно. -
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 # Последовательно (для тестов с побочными эффектами)
Краткая справка по тестированию
| pytest | Rust | Примечания |
|---|---|---|
assert x == y | assert_eq!(x, y) | Равенство |
assert x != y | assert_ne!(x, y) | Неравенство |
assert condition | assert!(condition) | Логическое условие |
assert condition, "msg" | assert!(condition, "msg") | С сообщением |
pytest.raises(E) | #[should_panic] | Ожидается паника |
@pytest.fixture | Настройка в тесте или вспомогательной функции | Встроенных фикстур нет |
@pytest.mark.parametrize | Крейт rstest | Параметризованные тесты |
conftest.py | tests/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 | Примечания |
|---|---|---|---|
| JSON | json | serde_json | Типобезопасная сериализация |
| CSV | csv, pandas | csv | Потоковая обработка, мало памяти |
| YAML | pyyaml | serde_yaml | Файлы конфигурации |
| TOML | tomllib | toml | Файлы конфигурации |
| Валидация данных | pydantic | serde + собственная логика | Проверка на этапе компиляции |
| Дата и время | datetime | chrono | Полная поддержка часовых поясов |
| Регулярные выражения | re | regex | Очень быстро |
| UUID | uuid | uuid | Та же концепция |
Веб и сеть
| Задача | Python | Крейт Rust | Примечания |
|---|---|---|---|
| HTTP-клиент | requests | reqwest | Асинхронный в первую очередь |
| Веб-фреймворк | FastAPI/Flask | axum / actix-web | Очень быстро |
| WebSocket | websockets | tokio-tungstenite | Асинхронный |
| gRPC | grpcio | tonic | Полная поддержка |
| База данных (SQL) | sqlalchemy | sqlx / diesel | SQL с проверкой на этапе компиляции |
| Redis | redis-py | redis | Поддержка async |
CLI и система
| Задача | Python | Крейт Rust | Примечания |
|---|---|---|---|
| Аргументы CLI | argparse/click | clap | Макросы derive |
| Цветной вывод | colorama | colored | Цвета в терминале |
| Индикатор прогресса | tqdm | indicatif | Похожий UX |
| Отслеживание файлов | watchdog | notify | Кроссплатформенный |
| Логирование | logging | tracing | Структурированный, готов к async |
| Переменные окружения | os.environ | std::env + dotenvy | Поддержка .env |
| Подпроцессы | subprocess | std::process::Command | Встроен |
| Временные файлы | tempfile | tempfile | Одинаковое название! |
Тестирование
| Задача | Python | Крейт Rust | Примечания |
|---|---|---|---|
| Фреймворк тестов | pytest | Встроенный + rstest | cargo test |
| Моки | unittest.mock | mockall | На основе трейтов |
| Property-тестирование | hypothesis | proptest | Похожий API |
| Снапшот-тестирование | syrupy | insta | Подтверждение снимков |
| Бенчмарки | pytest-benchmark | criterion | Статистические |
| Покрытие кода | coverage.py | cargo-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/47 | 47/47 (без изменений) |
Ключевой урок: переписывать всё приложение не нужно. Найдите 5% кода, на который уходит 95% времени, перепишите его на Rust с PyO3, а всё остальное оставьте на Python. Команда перешла от «нам нужно добавить серверов» к «одного сервера достаточно».
Упражнения
🏋️ Упражнение: матрица решений по миграции (нажмите, чтобы раскрыть)
Задание: у вас есть веб-приложение на Python с такими компонентами. Для каждого решите: оставить на Python, переписать на Rust или мост PyO3. Обоснуйте каждый выбор.
- Обработчики маршрутов Flask (разбор запросов, ответы JSON)
- Генерация миниатюр изображений (ограничено CPU, 10 тыс. изображений в день)
- Запросы ORM к базе данных (SQLAlchemy)
- Разбор CSV для финансовых файлов по 2 ГБ (запускается ночью)
- Панель администратора (шаблоны Jinja2)
🔑 Решение
| Компонент | Решение | Обоснование |
|---|---|---|
| Обработчики маршрутов Flask | 🐍 Оставить на Python | Ограничено вводом-выводом, завязано на фреймворк, выигрыш от Rust мал |
| Генерация миниатюр изображений | 🦀 Мост PyO3 | Горячий путь, ограниченный CPU: API остаётся на Python, внутренности на Rust |
| Запросы ORM к базе данных | 🐍 Оставить на Python | SQLAlchemy зрелый, запросы ограничены вводом-выводом |
| Разбор 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
Десять привычек, которые стоит выработать
-
Используйте
matchдля перечислений вместоif isinstance()# Python # Rust if isinstance(shape, Circle): ... match shape { Shape::Circle(r) => ... } -
Доверяйте компилятору. Внимательно читайте сообщения об ошибках. Компилятор Rust — лучший среди всех языков. Он говорит, что не так, И как это исправить.
-
Предпочитайте
&strвместоStringв параметрах функций. Принимайте наиболее общий тип.&strработает и сString, и с строковыми литералами. -
Используйте итераторы вместо циклов по индексу. Цепочки итераторов идиоматичнее и часто быстрее, чем
for i in 0..vec.len(). -
Используйте
OptionиResultв полной мере. Не вызывайте.unwrap()везде. Используйте?,map,and_then,unwrap_or_else. -
Щедро используйте derive для трейтов.
#[derive(Debug, Clone, PartialEq)]должен стоять на большинстве структур. Это бесплатно и упрощает тестирование. -
Регулярно запускайте
cargo clippy. Он находит сотни проблем со стилем и корректностью. Относитесь к нему как кruffдля Rust. -
Не боритесь с borrow checker. Если вы с ним воюете, скорее всего, данные структурированы неправильно. Переделайте структуру, чтобы владение стало понятным.
-
Используйте перечисления для конечных автоматов. Вместо строковых флагов или булевых значений используйте перечисления. Компилятор следит, чтобы обрабатывалось каждое состояние.
-
Сначала клонируйте, потом оптимизируйте. На этапе изучения свободно используйте
.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
| Python | Rust | Глава |
|---|---|---|
list | Vec<T> | 5 |
dict | HashMap<K,V> | 5 |
set | HashSet<T> | 5 |
tuple | (T1, T2, ...) | 5 |
class | struct + impl | 5 |
@dataclass | #[derive(...)] | 5, 12a |
Enum | enum | 6 |
None | Option<T> | 6 |
raise/try/except | Result<T,E> + ? | 9 |
Protocol (PEP 544) | trait | 10 |
TypeVar | Обобщения <T> | 10 |
__dunder__ методы | Трейты (Display, Add и др.) | 10 |
lambda | |args| body | 12 |
генератор yield | impl Iterator | 12 |
| списочное включение | .map().filter().collect() | 12 |
@decorator | Функция высшего порядка или макрос | 12a, 15 |
asyncio | tokio | 13 |
threading | std::thread | 13 |
multiprocessing | rayon | 13 |
unittest.mock | mockall | 14a |
pytest | cargo test + rstest | 14a |
pip install | cargo add | 8 |
requirements.txt | Cargo.lock | 8 |
pyproject.toml | Cargo.toml | 8 |
with (контекстный менеджер) | Освобождение по области видимости (Drop) | 15 |
json.dumps/loads | serde_json | 15 |
Заключительные мысли для разработчиков на 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. В Ruststruct+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]явно помечает тестовые функции.
Дополнительные задания
Когда базовая версия заработает, попробуйте эти улучшения:
-
Добавьте
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, } } -
Добавьте цветной вывод: используйте крейт
coloredдля цветов в терминале (аналогcoloramaв Python). -
Добавьте сроки выполнения: добавьте поле
Option<NaiveDate>и фильтруйте просроченные задачи. -
Добавьте теги и категории: используйте
Vec<String>для тегов и фильтруйте через.iter().any(). -
Сделайте библиотеку и исполняемый файл: разделите код на
lib.rsиmain.rs, чтобы логику можно было переиспользовать (паттерн модулей, гл. 8).
Что вы попрактиковали
| Глава | Концепция | Где использовалась |
|---|---|---|
| Гл. 3 | Типы и переменные | Поля структуры Task, u32, String, bool |
| Гл. 5 | Коллекции | Vec<Task>, retain(), push() |
| Гл. 6 | Перечисления и match | Priority, 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 |
| Гл. 11 | From/Into | FromStr для Priority, .into() для преобразования ошибок |
| Гл. 12 | Итераторы | filter, map, find, count, collect |
| Гл. 14 | Тестирование | #[test], #[cfg(test)], макросы проверок |
🎓 Поздравляем! Если вы создали этот проект, вы использовали каждую ключевую концепцию Rust, которая разобрана в этой книге. Вы больше не программист на Python, который учит Rust, а разработчик на Rust, который знает Python.