Перейти к содержимому

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.

Пользуйся этой таблицей как разговорником. В последнем столбце — ссылка на урок этого сайта, который объясняет сторону 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) Срезы

Здесь многие разработчики на 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 для фоновых задач внутри процесса
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 См. Работа с файлами, Окружение и процессы

Вот небольшой класс на 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.

В Python любое значение может втихую оказаться None, и ты узнаёшь об этом, когда его используешь:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
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 {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => 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 не указан
неизвестно
▶ Попробуй сам

Компилятор Rust не даст вызвать .to_uppercase() у Option<String>, поэтому ошибка «'NoneType' object has no attribute» просто не может случиться. String в Rust — это всегда настоящая строка. Вспомогательные методы описаны в уроке Option, а короткая форма if let Some(email) = ... — в уроке if let и while let.

В 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: 3
cat: 1
end: 1
▶ Попробуй сам

Текст здесь оставлен на английском намеренно: w.len() в Rust считает байты, а не буквы, и каждая кириллическая буква занимает в UTF-8 два байта, так что фильтр «ровно 3 символа» с русскими словами работал бы иначе.

Цепочка читается слева направо, а не изнутри наружу, но идея та же. Как и выражение-генератор, она ленивая: ничего не происходит, пока collect(), sum(), count() или цикл for не протянет значения через неё. collect() должен знать, что строить, поэтому у loud указан тип Vec<String>. И обрати внимание на сортировку: dict в Python помнит порядок вставки, а HashMap в Rust — нет, поэтому перед выводом мы всегда сортируем. Сортировка разобрана в уроке Сортировка и поиск.

В стандартном 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: простых чисел — 2762
25000..50000: простых чисел — 2371
50000..75000: простых чисел — 2260
75000..100000: простых чисел — 2199
всего: 9592
▶ Попробуй сам

thread::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 пока не готовы к работе без него.

Чтобы пользоваться Rust, не обязательно уходить из Python. PyO3 (на английском) позволяет написать модуль Python на Rust, а maturin (на английском) собирает его в обычный wheel, который можно поставить через pip. Так сделаны Polars, pydantic-core и многие другие быстрые библиотеки Python.

Создай проект (внутри активированного виртуального окружения):

Terminal window
pip install maturin
maturin new --bindings pyo3 fastmath
cd fastmath

maturin сам пишет 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])) # 30
print(fastmath.word_count("the quick brown fox")) # 4
fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integer

PyO3 сам преобразует типы: list целых чисел из Python становится Vec<i64>, str становится &str, а doc-комментарии превращаются в docstring Python. Неверные типы превращаются в обычный TypeError Python. Когда будешь готов поделиться модулем, maturin build --release соберёт wheel, а maturin publish загрузит его на PyPI. Хорошее правило: сначала замерь, потом переноси на Rust только горячий цикл. Переход между Python и Rust немного стоит, поэтому один вызов, который делает много работы, лучше миллиона крошечных.

Все, кто приходит из 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` trait
3 | let mut b = a;
| - value moved here
4 | 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 этот маршрут тратит время там, где Rust сильнее всего отличается от того, что ты знаешь:

  1. Первые шаги и Инструменты и установка: установи Rust и запусти cargo new.
  2. Переменные, типы данных и переполнение целых чисел: типы — первая новая привычка.
  3. Функции, if/else и циклы: читаются быстро; обрати внимание на выражения и пропущенные точки с запятой.
  4. Владение, затем заимствование, затем срезы: самые важные для тебя страницы. Не торопись.
  5. Строки: String и &str, и почему строку нельзя индексировать.
  6. Структуры и перечисления, затем match и сопоставление с образцом.
  7. Option и Result, затем свои типы ошибок.
  8. Векторы, HashMap и HashSet.
  9. Замыкания и итераторы: твои генераторы списков и генераторы.
  10. Трейты, обобщения, популярные трейты и трейт-объекты.
  11. Модули, Cargo и крейты и тестирование.
  12. Времена жизни и умные указатели.
  13. Многопоточность, общее состояние и async/await.
  14. JSON и Serde, а потом собери CLI-утилиту на clap или веб-API.

В обоих упражнениях дан фрагмент на Python. Перенеси его на Rust.

Перенеси этот код на 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 никогда не превращает целое число в дробное у тебя за спиной.

Перенеси этот код на 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 для машинного обучения и работы с данными.