Rust для разработчиков на Python: практическое руководство
Если ты знаешь Python, многие идеи Rust тебе уже знакомы. В Rust есть циклы for по итераторам, кортежи, замыкания (как лямбды в Python, только полезнее), оператор match, который работает почти как match в Python, и цепочки итераторов, которые делают то же, что генераторы списков (list comprehensions). Rust даже разделяет некоторые вкусы Python: один очевидный способ форматировать код, дружелюбный инструмент для пакетов и сообщения об ошибках, которые изо всех сил стараются помочь.
Новое в основном связано с компилятором. Python проверяет типы во время работы программы, а Rust проверяет их все до запуска. Python убирает память сборщиком мусора, а Rust использует владение (ownership) и заимствование (borrowing), которые проверяются при компиляции. В Python есть None и исключения, а в Rust — Option и Result. Именно на эти три вещи уйдёт большая часть времени на обучение.
Так зачем разработчики на Python учат Rust? Честные причины такие:
- Скорость. Rust компилируется в нативный машинный код. Тяжёлая для процессора работа, которая медленно идёт на чистом Python (разбор данных, вычисления, большие циклы), на Rust обычно выполняется намного быстрее, без всяких трюков с оптимизацией.
- Настоящие потоки. В стандартном CPython из-за GIL код на Python в каждый момент выполняет только один поток. Потоки Rust работают на всех ядрах процессора одновременно, а компилятор останавливает гонки данных ещё до запуска программы.
- Ошибки находятся раньше. Опечатка в имени атрибута, забытый
Noneили функция, вызванная с аргументом не того типа: в Python ты узнаёшь о них, когда выполнится эта строка. В Rust программа не скомпилируется, пока ты их не исправишь. - Выбирать не обязательно. С PyO3 (на английском) и maturin можно написать медленную часть Python-проекта на Rust и делать ей
importиз Python, как любому другому модулю. Популярные инструменты для Python, например Polars, Ruff и uv, написаны на Rust.
Если тебе нужно более общее сравнение Rust с Python, Go и C++, загляни в урок Rust против C++, Go и Python.
Понятия языка: из Python в Rust
Заголовок раздела «Понятия языка: из Python в Rust»Пользуйся этой таблицей как разговорником. В последнем столбце — ссылка на урок этого сайта, который объясняет сторону Rust.
| Python | Rust | Подробнее |
|---|---|---|
| Динамические типы, проверяются во время работы | Статические типы, проверяются при компиляции. Большинство из них Rust выводит сам, так что писать типы приходится реже, чем кажется | Типы данных |
Подсказки типов (def f(x: int) -> str) |
Типы в сигнатурах функций обязательны, и компилятор их проверяет | Функции |
x = 5 |
let x = 5; (менять нельзя). let mut x = 5;, если значение нужно менять |
Переменные |
int (растёт сколько нужно) |
Целые числа фиксированного размера: i32, i64, u64, usize, i128… Они могут переполниться |
Типы данных, Переполнение целых чисел |
float |
f64 (или f32) |
Типы данных |
str |
String (владеющая, растущая строка) и &str (заимствованный взгляд на текст) |
Строки |
f-строки: f"Hi {name}" |
format!("Hi {name}") и println!("Hi {name}") |
Вывод на экран |
print(repr(x)) / __repr__ |
println!("{x:?}") вместе с #[derive(Debug)] |
Вывод на экран, Популярные трейты |
__str__ |
Реализуй Display (и .to_string() тоже заработает) |
Популярные трейты |
__eq__, __hash__, __lt__ |
Derive для PartialEq, Eq, Hash, PartialOrd, Ord |
Популярные трейты |
None, Optional[T] |
Option<T>: либо Some(value), либо None |
Option |
x or default |
x.unwrap_or(default) |
Option |
raise, try/except |
Result<T, E> + оператор ? |
Result, Свои типы ошибок |
Исключение для настоящей ошибки в коде (assert, IndexError) |
panic! (останавливает поток) |
Паника |
class с __init__ и методами |
struct для данных + блок impl для методов; new — обычная функция |
Структуры |
@dataclass |
struct с #[derive(Debug, Clone, PartialEq)] |
Структуры |
self |
&self (чтение), &mut self (изменение) или self (забрать владение) |
Структуры |
| Наследование | Наследования нет. Используй композицию (структура внутри структуры) + трейты с методами по умолчанию | Трейты, Паттерны проектирования |
Утиная типизация, Protocol, ABC |
trait |
Трейты |
| Список объектов разных классов | Vec<Box<dyn Trait>> (трейт-объект) |
Трейт-объекты |
TypeVar / обобщённые функции |
Обобщения с ограничениями трейтов: fn largest<T: Ord>(items: &[T]) |
Обобщения |
Класс Enum |
enum, у которого каждый вариант может нести свои данные |
Перечисления |
match / case (3.10+) |
match (компилятор проверяет, что ты учёл все случаи) |
match, Сопоставление с образцом |
if my_list: (истинность) |
Пиши явно: if !my_list.is_empty(). В if можно поставить только bool |
if/else |
for i in range(10) |
for i in 0..10 |
Цикл for |
enumerate(xs), zip(a, b) |
xs.iter().enumerate(), a.iter().zip(b.iter()) |
Итераторы |
lambda x: x + 1 |
Замыкания: |x| x + 1 (и они могут занимать много строк) |
Замыкания |
Генераторы списков, map, filter |
Адаптеры итераторов: filter, map, затем collect() |
Итераторы |
Генераторы и yield |
Свой тип, реализующий Iterator, или std::iter::from_fn |
Свои итераторы |
with open(...) as f: |
Ничего писать не нужно: файлы и блокировки закрываются, когда их владелец выходит из области видимости (Drop) |
Область видимости, Работа с файлами |
Аргументы по умолчанию, *args, **kwargs |
Не поддерживаются. Используй параметры Option, срезы (&[T]), структуру с Default или билдер |
Функции, Паттерны проектирования |
| Декораторы | Прямого аналога нет. Оборачивай замыканиями или используй атрибутные макросы (например, #[tokio::main]) |
Замыкания, Процедурные макросы |
import и модули |
mod и use |
Модули |
| Пакет на PyPI | Крейт на crates.io | Cargo и крейты |
if __name__ == "__main__": |
fn main() |
Первые шаги |
threading (с GIL) |
std::thread: настоящие параллельные потоки |
Многопоточность |
multiprocessing, concurrent.futures |
Потоки или крейт rayon для параллельных итераторов |
Многопоточность, Общее состояние |
asyncio, async def, await |
async fn и .await со средой выполнения вроде tokio |
Async/Await |
7 // 2, 2 ** 10 |
7 / 2 (для целых чисел это целочисленное деление), 2_i64.pow(10) |
Операторы |
int("42"), str(42) |
"42".parse::<i32>() (возвращает Result), 42.to_string() |
Приведение типов, Преобразования |
Коллекции
Заголовок раздела «Коллекции»У всех встроенных коллекций Python есть место в стандартной библиотеке Rust. Главные отличия: коллекция в Rust хранит один тип (Vec<i32> хранит только i32), а HashMap не помнит порядок вставки.
| Python | Rust | Подробнее |
|---|---|---|
list |
Vec<T> |
Векторы |
tuple |
Кортежи (i32, String) или небольшая struct с именованными полями |
Кортежи |
dict |
HashMap<K, V> (без фиксированного порядка!) |
HashMap |
dict, когда нужны отсортированные ключи |
BTreeMap<K, V> |
HashMap |
dict, когда нужен порядок вставки |
Крейт indexmap |
|
set, frozenset |
HashSet<T>, BTreeSet<T> |
HashSet |
collections.Counter, defaultdict |
HashMap с entry API: *map.entry(k).or_insert(0) += 1 |
HashMap |
collections.deque |
VecDeque<T> |
VecDeque и BinaryHeap |
heapq (сначала наименьший) |
BinaryHeap<T> (сначала наибольший; оберни элементы в Reverse, чтобы сначала был наименьший) |
VecDeque и BinaryHeap |
namedtuple |
struct |
Структуры |
bytes, bytearray |
&[u8], Vec<u8> |
Срезы |
Срезы: xs[1:3] |
Срезы: &xs[1..3] (отрицательных индексов нет; используй xs.len() - 1) |
Срезы |
Инструменты: из pip в cargo
Заголовок раздела «Инструменты: из pip в cargo»Здесь многие разработчики на Python немного выдыхают. В Rust есть один официальный инструмент, Cargo, который делает работу pip, venv, setuptools, pytest и не только. Споров «каким инструментом для пакетов пользоваться?» просто нет.
| Python | Rust | Примечания |
|---|---|---|
Интерпретатор python |
Компилятор rustc, который обычно вызывают через cargo |
Сначала компилируешь, потом запускаешь нативный исполняемый файл. См. Первые шаги |
| pyenv | rustup (и файл rust-toolchain.toml, чтобы закрепить версию) |
См. Инструменты и установка |
| pip, poetry, uv | cargo |
Собирает, запускает, тестирует, добавляет пакеты, публикует. См. Cargo и крейты |
pip install requests |
cargo add reqwest |
Добавляет крейт в Cargo.toml только для этого проекта |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
Создаётся автоматически |
| venv / virtualenv | Не нужен | У каждого проекта свои зависимости, и собирается он в свою папку target/ |
| PyPI | crates.io | Документация по API каждого опубликованного крейта лежит на docs.rs |
python main.py |
cargo run (cargo run --release для полной скорости) |
Отладочные сборки работают намного медленнее; замеряй скорость в режиме release |
pipx install ruff |
cargo install ripgrep |
Устанавливает консольную утилиту |
| PyInstaller | cargo build --release |
Получаешь один нативный исполняемый файл; пользователям не нужно ставить Rust |
| black | cargo fmt |
Один стандартный стиль для всех |
| ruff, pylint, flake8 | cargo clippy |
Сотни полезных проверок, официально |
| mypy, pyright | Сам компилятор | Проверка типов не по желанию и не отдельный шаг |
| pytest, unittest | Встроенные #[test] и cargo test (cargo nextest — популярный более быстрый раннер) |
См. Тестирование |
Фикстуры pytest и parametrize |
rstest |
|
| hypothesis | proptest |
Тестирование на основе свойств. См. Тестирование: дополнительно |
unittest.mock |
mockall или написанные вручную фейки, реализующие твой трейт |
|
| timeit, pytest-benchmark | criterion |
См. Производительность |
| cProfile, py-spy | cargo flamegraph, perf или любой нативный профилировщик |
|
| Docstrings + Sphinx | Doc-комментарии /// в Markdown + cargo doc |
Примеры кода в документации запускаются как тесты. См. Документация |
| REPL Python, Jupyter | Rust Playground; evcxr_repl и evcxr_jupyter |
Встроенного REPL в Rust нет. Для экспериментов обычно делают маленькие проекты и запускают cargo run |
| VS Code + Pylance, PyCharm | VS Code + rust-analyzer или JetBrains RustRover | rust-analyzer даёт автодополнение, переход к определению и ошибки прямо в коде |
Вот небольшой Cargo.toml, чтобы сравнить его с pyproject.toml:
[package]name = "weather-cli"version = "0.1.0"edition = "2021"
[dependencies]reqwest = { version = "0.13", features = ["blocking", "json"] }serde = { version = "1", features = ["derive"] }clap = { version = "4", features = ["derive"] }Библиотеки и фреймворки: «Что использовать вместо…?»
Заголовок раздела «Библиотеки и фреймворки: «Что использовать вместо…?»»«Я пользуюсь requests, FastAPI и pandas. А что есть в Rust?» Вот ближайшие аналоги. Экосистема Python намного старше и больше, поэтому в некоторых областях (особенно в data science) у Rust вариантов меньше, и они моложе.
Веб и сеть
Заголовок раздела «Веб и сеть»| Python | Rust | Примечания |
|---|---|---|
| Flask, FastAPI | axum (мы делаем такой в проекте веб-API), actix-web | Обработчики — обычные async fn; типы запросов и ответов берутся из твоих структур, немного похоже на FastAPI с pydantic |
| Django | Полного аналога нет. Loco (loco-rs) — фреймворк «всё включено»; большинство команд собирает axum + ORM + шаблонизатор |
Встроенной админки нет |
| requests, httpx | reqwest (асинхронный, есть необязательный блокирующий клиент), ureq (простой и блокирующий) |
См. Работа с сетью |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja (от автора самого Jinja), tera, askama (шаблоны проверяются при компиляции) |
|
| BeautifulSoup | scraper |
Данные и базы данных
Заголовок раздела «Данные и базы данных»| Python | Rust | Примечания |
|---|---|---|
| SQLAlchemy ORM | Diesel (запросы проверяются на типы при компиляции), SeaORM (асинхронный, в стиле сущностей) | В уроке Базы данных оба показаны на работающем коде |
| psycopg, сырой SQL | sqlx: обычный SQL, который проверяется на твоей настоящей базе данных при компиляции | Асинхронный, очень популярный, не ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | apalis для очередей задач; tokio::spawn для фоновых задач внутри процесса |
Data science и машинное обучение
Заголовок раздела «Data science и машинное обучение»| Python | Rust | Примечания |
|---|---|---|
| pandas | Polars | Написан на Rust; его Python API, который ты, возможно, уже знаешь, построен на той же библиотеке на Rust |
| NumPy | ndarray |
N-мерные массивы со срезами и broadcasting; дополнительных возможностей намного меньше, чем в NumPy |
| scikit-learn | linfa, smartcore |
Меньше алгоритмов и намного меньше сообщества |
| PyTorch | candle (крейт candle-core), burn |
Хороши для запуска моделей; обучение по большей части всё ещё делают на Python |
| matplotlib | plotters |
|
Модуль csv |
Крейт csv + serde |
Повседневные строительные блоки
Заголовок раздела «Повседневные строительные блоки»| Python | Rust | Примечания |
|---|---|---|
json |
serde + serde_json |
Стандарт; также умеет TOML, YAML и другое. См. JSON и Serde |
| pydantic | serde для разбора + validator или garde для правил валидации |
Твоя структура и есть схема |
| argparse, click, typer | clap |
Аргументы описываются структурой. См. Проект: CLI-утилита на clap |
logging |
tracing или log + env_logger |
См. Логирование |
re |
regex |
Нет lookaround и обратных ссылок, зато гарантированно линейное время; в fancy-regex они есть. См. Регулярные выражения |
datetime |
chrono или jiff |
См. Дата и время |
random |
rand |
См. Случайные числа |
asyncio |
tokio |
См. Async/Await |
multiprocessing.Pool.map |
rayon: замени .iter() на .par_iter() |
|
itertools |
Большая часть уже встроена в Iterator; крейт itertools добавляет остальное |
|
os, pathlib, subprocess |
std::env, std::path, std::fs, std::process |
См. Работа с файлами, Окружение и процессы |
Бок о бок: класс становится struct + impl
Заголовок раздела «Бок о бок: класс становится struct + impl»Вот небольшой класс на Python:
class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
def deposit(self, amount): self.balance += amount
def __str__(self): return f"у {self.owner} на счёте {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)И то же самое на Rust:
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` — просто соглашение, а не ключевое слово. Аргументов по умолчанию нет, // поэтому вызывающий код всегда передаёт оба значения. fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self`: этот метод изменяет счёт. fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// Это __str__ в Rust.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "у {} на счёте {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}Вывод:
у Ada на счёте 150Отличий несколько. Поля объявлены заранее вместе с типами, поэтому нельзя потом добавить новый атрибут, как Python разрешает написать account.nickname = "A". Данные (struct) и поведение (impl) живут в отдельных блоках. Каждый метод сразу говорит, только ли он читает значение (&self) или изменяет его (&mut self), а поскольку deposit изменяет счёт, переменная должна быть объявлена как let mut. Аргументов по умолчанию в Rust нет: если нужен счёт, который начинается с нуля, добавь вторую функцию, например fn empty(owner: &str) -> Self.
Бок о бок: утиная типизация становится трейтами
Заголовок раздела «Бок о бок: утиная типизация становится трейтами»Python всё равно, какого класса объект, — лишь бы у него был нужный метод:
class Duck: def speak(self): return "Кря"
class Robot: def speak(self): return "Бип-буп"
def introduce(thing): print(f"Говорит: {thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError, но только когда выполнится эта строкаВ Rust «есть метод speak» записывается как трейт (trait), и каждый тип явно подключается к нему блоком impl:
trait Speak { fn speak(&self) -> String;}
struct Duck;struct Robot;
impl Speak for Duck { fn speak(&self) -> String { "Кря".to_string() }}
impl Speak for Robot { fn speak(&self) -> String { "Бип-буп".to_string() }}
// Принимает любой тип, который реализует Speak.fn introduce(thing: &dyn Speak) { println!("Говорит: {}", thing.speak());}
fn main() { let things: Vec<Box<dyn Speak>> = vec![Box::new(Duck), Box::new(Robot)]; for thing in &things { introduce(thing.as_ref()); }
// introduce(&42); // ошибка компиляции: `{integer}` не реализует Speak}Вывод:
Говорит: КряГоворит: Бип-бупПо духу это всё та же утиная типизация, только её проверяет компилятор: introduce(&42) — ошибка ещё до запуска программы, а не AttributeError в продакшене. Если ты пользовался typing.Protocol, трейты покажутся естественными. Для списка из разных типов нужен Box<dyn Speak>, потому что Rust должен знать, как вызвать метод у каждого элемента (см. Трейт-объекты). А ещё свой трейт можно реализовать для типов, которые писал не ты, например для String или i32.
Бок о бок: None становится Option
Заголовок раздела «Бок о бок: None становится Option»В Python любое значение может втихую оказаться None, и ты узнаёшь об этом, когда его используешь:
def find_email(user_id): return emails.get(user_id) # вернёт None, если такого нет
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'В Rust значение, которого может не быть, имеет тип Option<T>. Его нельзя использовать как обычное значение, пока ты не обработал случай None:
fn find_email(user_id: u32) -> Option<String> { match user_id { _ => None, }}
fn main() { for id in [1, 3] { match find_email(id) { Some(email) => println!("{id}: {}", email.to_uppercase()), None => println!("{id}: email не указан"), } }
// Как `find_email(3) or "неизвестно"` в Python. let email = find_email(3).unwrap_or("неизвестно".to_string()); println!("{email}");
// Изменить значение внутри, если оно есть. let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}Вывод:
3: email не указаннеизвестноSome("[email protected]")Компилятор Rust не даст вызвать .to_uppercase() у Option<String>, поэтому ошибка «'NoneType' object has no attribute» просто не может случиться. String в Rust — это всегда настоящая строка. Вспомогательные методы описаны в уроке Option, а короткая форма if let Some(email) = ... — в уроке if let и while let.
Бок о бок: исключения становятся Result и ?
Заголовок раздела «Бок о бок: исключения становятся Result и ?»В Python int() бросает исключение, и ты ловишь его где-то выше по стеку вызовов:
def parse_port(text): port = int(text) # может бросить ValueError if not 1 <= port <= 65535: raise ValueError(f"порт {port} вне допустимого диапазона") return port
for text in ["8080", "http", "70000"]: try: print(f"порт {parse_port(text)}") except ValueError as e: print(f"ошибка: {e}")В Rust функция, которая может завершиться неудачей, говорит об этом в типе возвращаемого значения. Оператор ? означает «если здесь произошла ошибка, сразу верни её вызывающему». Он делает то же, что всплытие исключения, но это видно в коде:
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} — не число ({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("порт {port} вне допустимого диапазона")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("порт {port}"), Err(e) => println!("ошибка: {e}"), } }}Вывод:
порт 8080ошибка: "http" — не число (invalid digit found in string)ошибка: порт 70000 вне допустимого диапазонаТекст invalid digit found in string («в строке недопустимая цифра») печатает сама стандартная библиотека Rust, поэтому он на английском.
Главное отличие: в Python любая функция может бросить исключение, и сигнатура об этом ничего не говорит. В Rust Result — часть сигнатуры, поэтому вызывающий код не сможет о нём забыть. Пока учишься, String в роли типа ошибки вполне подходит; настоящие проекты используют свои перечисления ошибок (Свои типы ошибок) или крейт anyhow, который очень похож на «бросить любое исключение» (anyhow и thiserror).
Бок о бок: генераторы списков и dict становятся итераторами и HashMap
Заголовок раздела «Бок о бок: генераторы списков и dict становятся итераторами и HashMap»Генераторы списков и подсчёт через dict — повседневный Python:
text = "the cat sat on the mat the end"
loud = [w.upper() for w in text.split() if len(w) == 3]print(loud)
counts = {}for word in text.split(): counts[word] = counts.get(word, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]for word, n in top: print(f"{word}: {n}")В Rust генератор списка становится цепочкой итераторов, которая заканчивается на collect(), а dict превращается в HashMap:
use std::collections::HashMap;
fn main() { let text = "the cat sat on the mat the end";
// [w.upper() for w in text.split() if len(w) == 3] let loud: Vec<String> = text .split_whitespace() .filter(|w| w.len() == 3) .map(|w| w.to_uppercase()) .collect(); println!("{loud:?}");
// counts[word] = counts.get(word, 0) + 1 let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; }
// Сначала самые частые; при равенстве — по алфавиту. let mut top: Vec<(&str, u32)> = counts.into_iter().collect(); top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0))); for (word, n) in top.iter().take(3) { println!("{word}: {n}"); }}Вывод:
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1Текст здесь оставлен на английском намеренно: w.len() в Rust считает байты, а не буквы, и каждая кириллическая буква занимает в UTF-8 два байта, так что фильтр «ровно 3 символа» с русскими словами работал бы иначе.
Цепочка читается слева направо, а не изнутри наружу, но идея та же. Как и выражение-генератор, она ленивая: ничего не происходит, пока collect(), sum(), count() или цикл for не протянет значения через неё. collect() должен знать, что строить, поэтому у loud указан тип Vec<String>. И обрати внимание на сортировку: dict в Python помнит порядок вставки, а HashMap в Rust — нет, поэтому перед выводом мы всегда сортируем. Сортировка разобрана в уроке Сортировка и поиск.
Бок о бок: GIL против настоящих потоков
Заголовок раздела «Бок о бок: GIL против настоящих потоков»В стандартном CPython глобальная блокировка интерпретатора (GIL) позволяет только одному потоку за раз выполнять байт-код Python. Потоки помогают, когда ждёшь сеть, но тяжёлая для процессора работа вроде этой от дополнительных потоков быстрее не становится, поэтому разработчики на Python берут multiprocessing:
from concurrent.futures import ThreadPoolExecutor
def is_prime(n): if n < 2: return False d = 2 while d * d <= n: if n % d == 0: return False d += 1 return True
def count_primes(lo, hi): return sum(1 for n in range(lo, hi) if is_prime(n))
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]with ThreadPoolExecutor(4) as pool: counts = list(pool.map(lambda r: count_primes(*r), ranges))print("всего:", sum(counts))В Rust нет GIL. Каждый thread::spawn — настоящий поток операционной системы, и потоки одновременно работают на разных ядрах процессора:
use std::thread;
fn is_prime(n: u32) -> bool { if n < 2 { return false; } let mut d = 2; while d * d <= n { if n % d == 0 { return false; } d += 1; } true}
fn count_primes(lo: u32, hi: u32) -> usize { (lo..hi).filter(|&n| is_prime(n)).count()}
fn main() { let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
// Каждый диапазон получает свой поток ОС. Они действительно работают одновременно. let counts: Vec<usize> = thread::scope(|s| { let handles: Vec<_> = ranges .iter() .map(|&(lo, hi)| s.spawn(move || count_primes(lo, hi))) .collect(); handles.into_iter().map(|h| h.join().unwrap()).collect() });
for ((lo, hi), count) in ranges.iter().zip(&counts) { println!("{lo}..{hi}: простых чисел — {count}"); } println!("всего: {}", counts.iter().sum::<usize>());}Вывод:
0..25000: простых чисел — 276225000..50000: простых чисел — 237150000..75000: простых чисел — 226075000..100000: простых чисел — 2199всего: 9592thread::scope ждёт все потоки, прежде чем вернуть управление, поэтому потоки могут безопасно заимствовать локальные данные. Без GIL можно забеспокоиться: а вдруг два потока одновременно изменят одно и то же значение? Rust решает это при компиляции: общие изменяемые данные нужно обернуть во что-то вроде Arc<Mutex<T>>, иначе программа не скомпилируется (см. Общее состояние). В настоящих проектах крейт rayon делает это ещё короче, немного похоже на multiprocessing.Pool.map, только на потоках:
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Python здесь тоже меняется: в Python 3.13 появилась необязательная сборка «free-threaded» без GIL. Но стандартный CPython, который ставит и разворачивает большинство людей, всё ещё с GIL, и многие расширения на C пока не готовы к работе без него.
Расширения для Python на Rust (PyO3 + maturin)
Заголовок раздела «Расширения для Python на Rust (PyO3 + maturin)»Чтобы пользоваться Rust, не обязательно уходить из Python. PyO3 (на английском) позволяет написать модуль Python на Rust, а maturin (на английском) собирает его в обычный wheel, который можно поставить через pip. Так сделаны Polars, pydantic-core и многие другие быстрые библиотеки Python.
Создай проект (внутри активированного виртуального окружения):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin сам пишет Cargo.toml и pyproject.toml. Затем положи свои функции на Rust в src/lib.rs:
use pyo3::prelude::*;
/// Модуль Python, написанный на Rust.#[pymodule]mod fastmath { use pyo3::prelude::*;
/// Складывает квадраты чисел из списка. #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// Считает слова в тексте. #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}Собери его в своё виртуальное окружение командой maturin develop (добавь --release, когда важна скорость) и пользуйся им из Python, как любым другим модулем:
import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integerPyO3 сам преобразует типы: list целых чисел из Python становится Vec<i64>, str становится &str, а doc-комментарии превращаются в docstring Python. Неверные типы превращаются в обычный TypeError Python. Когда будешь готов поделиться модулем, maturin build --release соберёт wheel, а maturin publish загрузит его на PyPI. Хорошее правило: сначала замерь, потом переноси на Rust только горячий цикл. Переход между Python и Rust немного стоит, поэтому один вызов, который делает много работы, лучше миллиона крошечных.
Обо что споткнётся разработчик на Python
Заголовок раздела «Обо что споткнётся разработчик на Python»Все, кто приходит из Python, натыкаются на одни и те же кочки. Знать их заранее полезно.
Присваивание перемещает значение
Заголовок раздела «Присваивание перемещает значение»В Python b = a даёт два имени для одного и того же списка. В Rust let b = a; перемещает (move) владение Vec или String в b, и a больше нельзя использовать:
fn main() { let a = vec![1, 2, 3]; let mut b = a; b.push(4); println!("{a:?}");}error[E0382]: borrow of moved value: `a` --> src/main.rs:5:16 |2 | let a = vec![1, 2, 3]; | - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let mut b = a; | - value moved here4 | b.push(4);5 | println!("{a:?}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let mut b = a.clone(); | ++++++++Компилятор говорит, что значение a уже перемещено, и сам предлагает вызвать .clone(). Исправить можно заимствованием (let b = &a;, если нужно только читать) или осознанным настоящим копированием (a.clone()). Это одно правило, владение, — самая важная новая идея для тебя. Прочитай этот урок и урок Заимствование не спеша.
Остальной список
Заголовок раздела «Остальной список»- У целых чисел есть размер.
intв Python никогда не переполняется.i32в Rust заканчивается на 2 147 483 647; если выйти за предел, отладочная сборка запаникует. Для больших чисел выбирайi64илиu64и загляни в урок Переполнение целых чисел. - Деление и остаток работают иначе.
7 / 2в Rust равно3, потому что обе стороны — целые числа (как//в Python). Пиши7.0 / 2.0, чтобы получить3.5. Ещё-7 % 3в Rust равно-1, а в Python —2; чтобы получить ответ как в Python, используй(-7_i32).rem_euclid(3). - Числовые типы не смешиваются.
let x: f64 = 2.5 * 3;не скомпилируется, потому что3— целое число. Пиши3.0или преобразуй черезas f64(см. Приведение типов). - Строку нельзя индексировать.
word[0]— ошибка компиляции, потому что строки в Rust хранятся в UTF-8, и «символ» может занимать несколько байтов. Используйword.chars().next()илиword.chars().nth(i). См. Строки. Stringи&str. В Python один типstr. В Rust два:Stringвладеет своим текстом,&strзаимствует его. Функции обычно принимают&str, а структуры обычно хранятString.- Переменные по умолчанию неизменяемые. Пиши
let mut, когда значение должно меняться. Компилятор подскажет, если забудешь. - Фигурные скобки и точки с запятой. Отступы для Rust не важны, важны
{ }. А последняя строка функции без точки с запятой — это её возвращаемое значение:fn double(x: i32) -> i32 { x * 2 }. - Каждый
ifиmatchдолжен быть полным и правильным по типам. Истинности нет:if countне скомпилируется, нужно писатьif count > 0.matchдолжен покрывать все случаи. - У HashMap нет порядка.
dictв Python помнит порядок вставки, аHashMapв Rust — нет. Сортируй перед выводом, используйBTreeMapили добавь крейтindexmap. - Есть шаг компиляции. REPL нет, а чистая сборка проекта с множеством крейтов занимает время. Пока редактируешь, запускай
cargo check(он пропускает генерацию кода и работает намного быстрее), а для замеров скорости —cargo run --release. - Анализатор заимствований будет сопротивляться. Обычные для Python схемы — например, много объектов, которые держат и меняют один общий объект, или объекты, которые ссылаются обратно на родителя, — требуют умных указателей или другой архитектуры. Приготовься к нескольким неделям раздражения. Потом станет намного легче.
Путь обучения для разработчиков на Python
Заголовок раздела «Путь обучения для разработчиков на Python»Этот сайт не обязательно читать по порядку. Для разработчика на Python этот маршрут тратит время там, где Rust сильнее всего отличается от того, что ты знаешь:
- Первые шаги и Инструменты и установка: установи Rust и запусти
cargo new. - Переменные, типы данных и переполнение целых чисел: типы — первая новая привычка.
- Функции, if/else и циклы: читаются быстро; обрати внимание на выражения и пропущенные точки с запятой.
- Владение, затем заимствование, затем срезы: самые важные для тебя страницы. Не торопись.
- Строки:
Stringи&str, и почему строку нельзя индексировать. - Структуры и перечисления, затем match и сопоставление с образцом.
- Option и Result, затем свои типы ошибок.
- Векторы, HashMap и HashSet.
- Замыкания и итераторы: твои генераторы списков и генераторы.
- Трейты, обобщения, популярные трейты и трейт-объекты.
- Модули, Cargo и крейты и тестирование.
- Времена жизни и умные указатели.
- Многопоточность, общее состояние и async/await.
- JSON и Serde, а потом собери CLI-утилиту на clap или веб-API.
Практика
Заголовок раздела «Практика»В обоих упражнениях дан фрагмент на Python. Перенеси его на Rust.
1. Перенеси генератор списка и dict
Заголовок раздела «1. Перенеси генератор списка и dict»Перенеси этот код на Python. Подсказка: чтобы вывести имена по порядку, собери их в Vec<&str>, вызови .sort(), а затем .join(", "). Для среднего значения преобразуй через as f64 до деления.
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("сдали:", ", ".join(passed))
odd_squares = [n * n for n in range(1, 10) if n % 2 == 1]print(odd_squares)
average = sum(scores.values()) / len(scores)print(f"среднее: {average:.1f}")Показать решение
use std::collections::HashMap;
fn main() { let scores: HashMap<&str, u32> = HashMap::from([("Ada", 92), ("Bob", 47), ("Cy", 75), ("Dee", 38)]);
let mut passed: Vec<&str> = scores .iter() .filter(|(_, score)| **score >= 50) .map(|(name, _)| *name) .collect(); passed.sort(); println!("сдали: {}", passed.join(", "));
let odd_squares: Vec<u32> = (1..10).filter(|n| n % 2 == 1).map(|n| n * n).collect(); println!("{odd_squares:?}");
let total: u32 = scores.values().sum(); let average = total as f64 / scores.len() as f64; println!("среднее: {average:.1}");}Вывод:
сдали: Ada, Cy[1, 9, 25, 49, 81]среднее: 63.0При обходе HashMap ты получаешь пары ссылок, поэтому здесь видны **score и *name: они идут по ссылкам обратно к значениям. sort() здесь не по желанию: без него имена могли бы выйти в любом порядке. А total as f64 нужен потому, что Rust никогда не превращает целое число в дробное у тебя за спиной.
2. Перенеси try/except на Result
Заголовок раздела «2. Перенеси try/except на Result»Перенеси этот код на Python. Каждая строка должна иметь вид имя: возраст. Подсказка: line.split_once(':') возвращает Option<(&str, &str)>; преврати его в Result с помощью .ok_or(...), а затем используй ?. Пусть твоя функция возвращает Result<(String, u32), String>.
def parse_line(line): if ":" not in line: raise ValueError("нет ':'") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"неверный возраст: {e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: сорок"]: try: name, age = parse_line(line) ages[name] = age print(f"ок: {name}, возраст {age}") except ValueError as e: print(f"пропущено {line!r}: {e}")print("сохранено:", len(ages))Показать решение
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("нет ':'")?; let age: u32 = age.trim().parse().map_err(|e| format!("неверный возраст: {e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: сорок"] { match parse_line(line) { Ok((name, age)) => { println!("ок: {name}, возраст {age}"); ages.insert(name, age); } Err(e) => println!("пропущено {line:?}: {e}"), } }
println!("сохранено: {}", ages.len());}Вывод:
ок: Ada, возраст 36пропущено "Grace 45": нет ':'пропущено "Linus: сорок": неверный возраст: invalid digit found in stringсохранено: 1Оба raise стали значениями Err, а try/except — выражением match. ? после ok_or("нет ':'") работает, хотя ошибка здесь — &str, а функция возвращает ошибки типа String: ? сам преобразует ошибку через From. Обрати внимание на порядок внутри Ok: сначала мы печатаем, а потом insert перемещает name в словарь. Поменяй эти две строки местами, и компилятор скажет, что name уже перемещено. Текст invalid digit found in string — сообщение стандартной библиотеки, поэтому оно на английском.
- Многое из Python переносится: циклы по итераторам, кортежи, замыкания,
matchи обработка данных в стиле генераторов списков (в виде цепочек итераторов). - Главные сдвиги: статические типы, проверяемые до запуска, владение вместо сборщика мусора,
OptionвместоNoneиResult+?вместо исключений. - Классы становятся
struct+impl, утиная типизация — трейтами, а блокиwith— автоматическимDrop. УHashMapнет порядка, а у целых чисел фиксированный размер. - В Rust нет GIL: потоки работают параллельно, а компилятор не допускает гонок данных.
- Cargo заменяет pip, venv и компанию;
cargo fmt,cargo clippyиcargo testзаменяют black, ruff и pytest. - Языки можно совмещать: PyO3 и maturin превращают код на Rust в модуль Python.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что Rust использует вместо None из Python?
2. В Python b = a даёт два имени для одного списка. Что делает let b = a; с Vec в Rust?
3. Чему равно 7 / 2 в Rust, если оба числа целые?
4. Ты хочешь ускорить одну медленную функцию в Python-проекте с помощью Rust. Какие инструменты тебе нужны?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Язык программирования Rust: официальная книга (перевод на русский). Для разработчиков на Python важнее всего главы 4 (владение), 6 (перечисления и
Option), 9 (ошибки) и 13 (замыкания и итераторы). - Руководство PyO3 (на английском): всё о вызове Rust из Python (и Python из Rust).
- Руководство maturin (на английском): сборка, тестирование и публикация пакетов Python на Rust.
- Rust на примерах: короткие запускаемые примеры (перевод на русский), удобно, если лучше всего учишься, читая код.
- Are We Learning Yet? (на английском): обзор крейтов Rust для машинного обучения и работы с данными.