Rust per sviluppatori Python: guida pratica
Se conosci Python, conosci già molte idee di Rust. Rust ha cicli for sugli iteratori, tuple, closure (le lambda di Python, ma più utili), un match che funziona come il match di Python, e catene di iteratori che fanno quello che fanno le list comprehension. Rust ha perfino preso un po’ del gusto di Python: un solo modo ovvio di formattare il codice, uno strumento per i pacchetti amichevole e messaggi di errore che fanno di tutto per aiutarti.
La novità riguarda soprattutto il compilatore. Python controlla i tipi mentre il programma gira; Rust li controlla tutti prima che parta. Python libera la memoria con un garbage collector; Rust usa l’ownership (possesso) e il prestito (borrowing), controllati in compilazione. Python ha None e le eccezioni; Rust ha Option e Result. È su queste tre cose che passerai la maggior parte del tempo di studio.
Ma perché uno sviluppatore Python dovrebbe imparare Rust? Le ragioni oneste sono queste:
- Velocità. Rust viene compilato in codice macchina nativo. Il lavoro pesante per la CPU che in Python puro è lento (parsing, calcoli numerici, grandi cicli) di solito in Rust è molto più veloce, senza trucchi di ottimizzazione.
- Thread veri. Nel CPython standard, il GIL lascia eseguire codice Python a un solo thread alla volta. I thread di Rust girano su tutti i core della CPU nello stesso momento, e il compilatore blocca le data race prima che il programma parta.
- Bug scoperti prima. Un errore di battitura nel nome di un attributo, un
Noneche avevi dimenticato, una funzione chiamata con il tipo sbagliato: in Python li scopri quando quella riga viene eseguita. In Rust il programma non compila finché non li correggi. - Non devi scegliere. Con PyO3 (in inglese) e maturin puoi scrivere in Rust la parte lenta di un progetto Python e importarla con
importda Python come qualsiasi altro modulo. Strumenti Python molto diffusi come Polars, Ruff e uv sono scritti in Rust.
Se invece cerchi un confronto generale di Rust con Python, Go e C++, guarda Rust vs C++, Go e Python.
Concetti del linguaggio: da Python a Rust
Sezione intitolata “Concetti del linguaggio: da Python a Rust”Usa questa tabella come un frasario. L’ultima colonna rimanda alla lezione di questo sito che spiega il lato Rust.
| Python | Rust | Per saperne di più |
|---|---|---|
| Tipi dinamici, controllati durante l’esecuzione | Tipi statici, controllati in compilazione. Rust ne deduce quasi tutti, quindi scrivi meno tipi di quanto pensi | Tipi di dati |
Type hint (def f(x: int) -> str) |
I tipi nelle firme delle funzioni sono obbligatori, e il compilatore li fa rispettare | Funzioni |
x = 5 |
let x = 5; (non si può cambiare). let mut x = 5; se vuoi cambiarlo |
Variabili |
int (cresce quanto serve) |
Interi di dimensione fissa: i32, i64, u64, usize, i128… Possono andare in overflow |
Tipi di dati, Overflow degli interi |
float |
f64 (o f32) |
Tipi di dati |
str |
String (posseduta, può crescere) e &str (una vista presa in prestito su un testo) |
Stringhe |
f-string: f"Hi {name}" |
format!("Hi {name}") e println!("Hi {name}") |
Output |
print(repr(x)) / __repr__ |
println!("{x:?}") con #[derive(Debug)] |
Output, Trait comuni |
__str__ |
Implementa Display (e poi funziona anche .to_string()) |
Trait comuni |
__eq__, __hash__, __lt__ |
Derive di PartialEq, Eq, Hash, PartialOrd, Ord |
Trait comuni |
None, Optional[T] |
Option<T>: o Some(valore) o None |
Option |
x or default |
x.unwrap_or(default) |
Option |
raise, try/except |
Result<T, E> + l’operatore ? |
Result, Errori personalizzati |
Un’eccezione per un vero bug (assert, IndexError) |
panic! (ferma il thread) |
Panic |
class con __init__ e metodi |
struct per i dati + un blocco impl per i metodi; new è una funzione normale |
Struct |
@dataclass |
struct con #[derive(Debug, Clone, PartialEq)] |
Struct |
self |
&self (legge), &mut self (modifica) o self (prende l’ownership) |
Struct |
| Ereditarietà | Niente ereditarietà. Usa la composizione (una struct dentro una struct) + trait con metodi predefiniti | Trait, Design pattern |
Duck typing, Protocol, ABC |
trait |
Trait |
| Una lista di oggetti di classi diverse | Vec<Box<dyn Trait>> (un trait object) |
Trait object |
TypeVar / funzioni generiche |
Generici con vincoli di trait (trait bound): fn largest<T: Ord>(items: &[T]) |
Generici |
Classe Enum |
enum, in cui ogni variante può portare i propri dati |
Enum |
match / case (3.10+) |
match (il compilatore controlla che tu abbia coperto ogni caso) |
match, Pattern matching |
if my_list: (valori «truthy») |
Sii esplicito: if !my_list.is_empty(). In un if va solo un bool |
if/else |
for i in range(10) |
for i in 0..10 |
Cicli for |
enumerate(xs), zip(a, b) |
xs.iter().enumerate(), a.iter().zip(b.iter()) |
Iteratori |
lambda x: x + 1 |
Closure: |x| x + 1 (e possono occupare molte righe) |
Closure |
List comprehension, map, filter |
Adattatori di iteratori: filter, map, poi collect() |
Iteratori |
Generatori e yield |
Un tuo tipo che implementa Iterator, o std::iter::from_fn |
Iteratori personalizzati |
with open(...) as f: |
Non devi scrivere niente: file e lock si chiudono quando il loro proprietario esce dallo scope (Drop) |
Scope, I/O su file |
Argomenti predefiniti, *args, **kwargs |
Non supportati. Usa parametri Option, slice (&[T]), una struct con Default, o un builder |
Funzioni, Design pattern |
| Decoratori | Nessun equivalente diretto. Avvolgi con le closure, o usa macro attributo (come #[tokio::main]) |
Closure, Macro procedurali |
import e moduli |
mod e use |
Moduli |
| Pacchetto su PyPI | Crate su crates.io | Cargo e crate |
if __name__ == "__main__": |
fn main() |
Primi passi |
threading (con il GIL) |
std::thread: thread davvero paralleli |
Concorrenza |
multiprocessing, concurrent.futures |
Thread, o il crate rayon per iteratori paralleli |
Concorrenza, Stato condiviso |
asyncio, async def, await |
async fn e .await, con un runtime come tokio |
Async/await |
7 // 2, 2 ** 10 |
7 / 2 (divisione intera sugli interi), 2_i64.pow(10) |
Operatori |
int("42"), str(42) |
"42".parse::<i32>() (restituisce un Result), 42.to_string() |
Conversione di tipo, Conversioni |
Collezioni
Sezione intitolata “Collezioni”Tutte le collezioni integrate di Python hanno un posto nella libreria standard di Rust. Le differenze più grandi: una collezione Rust contiene un solo tipo (un Vec<i32> contiene solo i32), e una HashMap non ricorda l’ordine di inserimento.
| Python | Rust | Per saperne di più |
|---|---|---|
list |
Vec<T> |
Vettori |
tuple |
Tuple (i32, String), o una piccola struct con campi con nome |
Tuple |
dict |
HashMap<K, V> (nessun ordine fisso!) |
HashMap |
dict quando ti servono le chiavi ordinate |
BTreeMap<K, V> |
HashMap |
dict quando ti serve l’ordine di inserimento |
Il crate indexmap |
|
set, frozenset |
HashSet<T>, BTreeSet<T> |
HashSet |
collections.Counter, defaultdict |
HashMap con l’API entry: *map.entry(k).or_insert(0) += 1 |
HashMap |
collections.deque |
VecDeque<T> |
VecDeque e BinaryHeap |
heapq (prima il più piccolo) |
BinaryHeap<T> (prima il più grande; avvolgi gli elementi in Reverse per avere prima il più piccolo) |
VecDeque e BinaryHeap |
namedtuple |
struct |
Struct |
bytes, bytearray |
&[u8], Vec<u8> |
Slice |
Slicing: xs[1:3] |
Slice: &xs[1..3] (niente indici negativi; usa xs.len() - 1) |
Slice |
Strumenti: da pip a cargo
Sezione intitolata “Strumenti: da pip a cargo”Qui molti sviluppatori Python tirano un sospiro di sollievo. Rust ha un solo strumento ufficiale, Cargo, che fa il lavoro di pip, venv, setuptools, pytest e altro ancora. Non esiste il dibattito «quale strumento di packaging usiamo?».
| Python | Rust | Note |
|---|---|---|
Interprete python |
Compilatore rustc, di solito chiamato tramite cargo |
Prima compili, poi esegui un binario nativo. Vedi Primi passi |
| pyenv | rustup (e un file rust-toolchain.toml per fissare una versione) |
Vedi Strumenti e configurazione |
| pip, poetry, uv | cargo |
Compila, esegue, testa, aggiunge pacchetti, pubblica. Vedi Cargo e crate |
pip install requests |
cargo add reqwest |
Aggiunge il crate al Cargo.toml solo per questo progetto |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
Viene creato per te |
| venv / virtualenv | Non serve | Ogni progetto ha le sue dipendenze e compila nella sua cartella target/ |
| PyPI | crates.io | La documentazione delle API di ogni crate pubblicato è su docs.rs (in inglese) |
python main.py |
cargo run (cargo run --release per la massima velocità) |
Le build di debug sono molto più lente in esecuzione; misura la velocità in modalità release |
pipx install ruff |
cargo install ripgrep |
Installa uno strumento da riga di comando |
| PyInstaller | cargo build --release |
Ottieni un unico binario nativo; gli utenti non devono installare Rust |
| black | cargo fmt |
Un solo stile standard per tutti |
| ruff, pylint, flake8 | cargo clippy |
Centinaia di lint utili, ufficiali |
| mypy, pyright | Il compilatore stesso | Il controllo dei tipi non è facoltativo e non è un passaggio separato |
| pytest, unittest | #[test] integrato e cargo test (cargo nextest è un runner più veloce molto usato) |
Vedi Test |
Fixture di pytest e parametrize |
rstest |
|
| hypothesis | proptest |
Test basati sulle proprietà. Vedi Test avanzati |
unittest.mock |
mockall, o finti oggetti scritti a mano che implementano il tuo trait |
|
| timeit, pytest-benchmark | criterion |
Vedi Prestazioni |
| cProfile, py-spy | cargo flamegraph, perf, o qualsiasi profiler nativo |
|
| Docstring + Sphinx | Commenti di documentazione /// in Markdown + cargo doc |
Gli esempi di codice nella documentazione vengono eseguiti come test. Vedi Documentazione |
| La REPL di Python, Jupyter | Il Rust Playground; evcxr_repl e evcxr_jupyter |
Rust non ha una REPL integrata. Il modo abituale per sperimentare sono piccoli progetti con cargo run |
| VS Code + Pylance, PyCharm | VS Code + rust-analyzer, o JetBrains RustRover | rust-analyzer offre completamento, vai-alla-definizione ed errori in linea |
Ecco un piccolo Cargo.toml, così puoi confrontarlo con un 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"] }Librerie e framework: «cosa usiamo al posto di…?»
Sezione intitolata “Librerie e framework: «cosa usiamo al posto di…?»”«Uso requests, FastAPI e pandas. Qual è la versione Rust?» Ecco gli equivalenti più vicini. L’ecosistema di Python è molto più vecchio e più grande, quindi in alcune aree (soprattutto la data science) Rust ha opzioni più giovani e meno numerose.
Web e rete
Sezione intitolata “Web e rete”| Python | Rust | Note |
|---|---|---|
| Flask, FastAPI | axum (ne costruiamo una in Progetto: API web), actix-web | Gli handler sono semplici async fn; i tipi di richiesta e risposta vengono dalle tue struct, un po’ come FastAPI con pydantic |
| Django | Nessun equivalente completo. Loco (loco-rs) è un framework «batterie incluse»; la maggior parte dei team combina axum + un ORM + un motore di template |
Nessun pannello di amministrazione integrato |
| requests, httpx | reqwest (asincrono, con un client bloccante opzionale), ureq (semplice e bloccante) |
Vedi Reti |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja (dello stesso autore di Jinja), tera, askama (template controllati in compilazione) |
|
| BeautifulSoup | scraper |
Dati e database
Sezione intitolata “Dati e database”| Python | Rust | Note |
|---|---|---|
| ORM di SQLAlchemy | Diesel (query controllate sui tipi in compilazione), SeaORM (asincrono, stile a entità) | Database in Rust li mostra entrambi con codice funzionante |
| psycopg, SQL scritto a mano | sqlx: SQL semplice, controllato sul tuo vero database in compilazione | Asincrono, molto popolare, non è un ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | apalis per le code di job; tokio::spawn per task in background nel processo |
Data science e machine learning
Sezione intitolata “Data science e machine learning”| Python | Rust | Note |
|---|---|---|
| pandas | Polars | Scritto in Rust; la sua API Python, che forse conosci già, è costruita sulla stessa libreria Rust |
| NumPy | ndarray |
Array a N dimensioni con slicing e broadcasting; molti meno extra di NumPy |
| scikit-learn | linfa, smartcore |
Meno algoritmi e comunità molto più piccole |
| PyTorch | candle (crate candle-core), burn |
Buoni per eseguire modelli; quasi tutto l’addestramento si fa ancora in Python |
| matplotlib | plotters |
|
Modulo csv |
Crate csv + serde |
I mattoni di tutti i giorni
Sezione intitolata “I mattoni di tutti i giorni”| Python | Rust | Note |
|---|---|---|
json |
serde + serde_json |
Lo standard; gestisce anche TOML, YAML e altro. Vedi JSON con Serde |
| pydantic | serde per il parsing + validator o garde per le regole di validazione |
La tua struct è lo schema |
| argparse, click, typer | clap |
Definisci gli argomenti come una struct. Vedi Progetto: tool CLI con clap |
logging |
tracing, o log + env_logger |
Vedi Logging |
re |
regex |
Niente look-around né backreference, ma tempo lineare garantito; fancy-regex li ha. Vedi Espressioni regolari |
datetime |
chrono o jiff |
Vedi Date e orari |
random |
rand |
Vedi Numeri casuali |
asyncio |
tokio |
Vedi Async/await |
multiprocessing.Pool.map |
rayon: cambia .iter() in .par_iter() |
|
itertools |
Quasi tutto è già integrato in Iterator; il crate itertools aggiunge il resto |
|
os, pathlib, subprocess |
std::env, std::path, std::fs, std::process |
Vedi I/O su file, Ambiente e processi |
Fianco a fianco: una classe diventa struct + impl
Sezione intitolata “Fianco a fianco: una classe diventa struct + impl”Ecco una piccola classe 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} has {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)E la stessa cosa in Rust:
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` è solo una convenzione, non una parola chiave. Niente argomenti // predefiniti, quindi chi chiama passa sempre entrambi i valori. fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self`: questo metodo modifica il conto. fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// Questo è il __str__ di Rust.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} ha {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}Output:
Ada ha 150Alcune cose sono diverse. I campi vengono dichiarati subito con i loro tipi, quindi non puoi aggiungere un nuovo attributo più tardi, come Python ti lascia fare con account.nickname = "A". Dati (la struct) e comportamento (l’impl) vivono in due blocchi separati. Ogni metodo dichiara se si limita a leggere (&self) o se modifica (&mut self) il valore, e siccome deposit modifica il conto, la variabile deve essere dichiarata let mut. Rust non ha argomenti predefiniti: se vuoi un conto che parte da zero, aggiungi una seconda funzione, come fn empty(owner: &str) -> Self.
Fianco a fianco: il duck typing diventa trait
Sezione intitolata “Fianco a fianco: il duck typing diventa trait”A Python non interessa di che classe sia un oggetto, solo che abbia il metodo giusto:
class Duck: def speak(self): return "Quack"
class Robot: def speak(self): return "Beep boop"
def introduce(thing): print(f"It says: {thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError, ma solo quando questa riga viene eseguitaIn Rust, «ha un metodo speak» si scrive come un trait, e ogni tipo aderisce con un blocco impl:
trait Speak { fn speak(&self) -> String;}
struct Duck;struct Robot;
impl Speak for Duck { fn speak(&self) -> String { "Quack".to_string() }}
impl Speak for Robot { fn speak(&self) -> String { "Beep boop".to_string() }}
// Accetta qualsiasi tipo che implementa Speak.fn introduce(thing: &dyn Speak) { println!("Dice: {}", 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); // errore di compilazione: `{integer}` non implementa Speak}Output:
Dice: QuackDice: Beep boopNello spirito è ancora duck typing, ma controllato dal compilatore: introduce(&42) è un errore prima ancora che il programma parta, non un AttributeError in produzione. Se hai usato typing.Protocol, i trait ti sembreranno naturali. Una lista di tipi diversi ha bisogno di Box<dyn Speak>, perché Rust deve sapere come chiamare il metodo su ogni elemento (vedi Trait object). E puoi implementare un tuo trait per tipi che non hai scritto tu, come String o i32.
Fianco a fianco: None diventa Option
Sezione intitolata “Fianco a fianco: None diventa Option”In Python qualsiasi valore può essere, di nascosto, None, e te ne accorgi quando lo usi:
def find_email(user_id): return emails.get(user_id) # restituisce None se manca
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'In Rust un valore che potrebbe mancare ha il tipo Option<T>. Non puoi usarlo come un valore normale finché non hai gestito il caso 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}: nessuna email registrata"), } }
// Come `find_email(3) or "unknown"` in Python. let email = find_email(3).unwrap_or("sconosciuta".to_string()); println!("{email}");
// Trasforma il valore contenuto, se c'è. let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}Output:
3: nessuna email registratasconosciutaSome("[email protected]")Il compilatore Rust non ti lascia chiamare .to_uppercase() su un Option<String>, quindi l’errore «'NoneType' object has no attribute» semplicemente non può capitare. Una String di Rust è sempre una stringa vera. Impara i metodi di supporto nella pagina Option, e la forma breve if let Some(email) = ... in if let.
Fianco a fianco: le eccezioni diventano Result e ?
Sezione intitolata “Fianco a fianco: le eccezioni diventano Result e ?”In Python int() lancia un’eccezione, e tu la catturi da qualche parte più in alto nello stack delle chiamate:
def parse_port(text): port = int(text) # può lanciare ValueError if not 1 <= port <= 65535: raise ValueError(f"port {port} is out of range") return port
for text in ["8080", "http", "70000"]: try: print(f"port {parse_port(text)}") except ValueError as e: print(f"error: {e}")In Rust una funzione che può fallire lo dichiara nel tipo di ritorno. L’operatore ? significa «se questa cosa è fallita, restituisci subito l’errore a chi mi ha chiamato». Fa lo stesso lavoro di un’eccezione che risale, ma lo vedi nel codice:
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} non è un numero ({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("la porta {port} è fuori intervallo")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("porta {port}"), Err(e) => println!("errore: {e}"), } }}Output:
porta 8080errore: "http" non è un numero (invalid digit found in string)errore: la porta 70000 è fuori intervalloIl testo tra parentesi, «invalid digit found in string» (cifra non valida nella stringa), arriva dalla libreria standard ed è sempre in inglese.
La grande differenza: in Python qualsiasi funzione potrebbe lanciare un’eccezione, e niente nella sua firma te lo dice. In Rust Result fa parte della firma, quindi chi chiama non può dimenticarsene. Mentre impari, String va benissimo come tipo di errore; i progetti veri usano i propri enum di errore (Errori personalizzati) o il crate anyhow, che assomiglia molto a «lancia un’eccezione qualsiasi» (anyhow e thiserror).
Fianco a fianco: comprehension e dict diventano iteratori e HashMap
Sezione intitolata “Fianco a fianco: comprehension e dict diventano iteratori e HashMap”Le list comprehension e il conteggio con un dict sono il pane quotidiano di 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}")In Rust una comprehension diventa una catena di iteratori che termina con collect(), e il dict diventa una 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; }
// Prima le più frequenti; a parità, in ordine alfabetico. 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}"); }}Output:
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1Il testo resta in inglese perché il risultato dipende dalla lunghezza delle parole, dalle maiuscole e dall’ordine alfabetico. La catena si legge da sinistra a destra invece che dall’interno verso l’esterno, ma l’idea è la stessa. Come un’espressione generatore, è pigra (lazy): non succede niente finché collect(), sum(), count() o un ciclo for non tirano fuori i valori. collect() deve sapere cosa costruire, ed è per questo che loud ha il tipo Vec<String>. E nota il passaggio di ordinamento: un dict di Python ricorda l’ordine di inserimento, una HashMap di Rust no, quindi ordiniamo sempre prima di stampare. L’ordinamento è spiegato nella pagina Ordinamento e ricerca.
Fianco a fianco: il GIL contro i thread veri
Sezione intitolata “Fianco a fianco: il GIL contro i thread veri”Nel CPython standard, il Global Interpreter Lock (GIL) lascia eseguire bytecode Python a un solo thread alla volta. I thread aiutano quando aspetti la rete, ma un lavoro pesante per la CPU come questo non diventa più veloce con più thread, così gli sviluppatori Python ricorrono a 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("total:", sum(counts))Rust non ha il GIL. Ogni thread::spawn è un vero thread del sistema operativo, e i thread girano su core diversi della CPU nello stesso momento:
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)];
// Ogni intervallo ha il suo thread del sistema operativo. Girano davvero in parallelo. 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} numeri primi"); } println!("totale: {}", counts.iter().sum::<usize>());}Output:
0..25000: 2762 numeri primi25000..50000: 2371 numeri primi50000..75000: 2260 numeri primi75000..100000: 2199 numeri primitotale: 9592thread::scope aspetta tutti i thread prima di restituire il controllo, quindi i thread possono prendere in prestito i dati locali senza rischi. Senza GIL potresti temere che due thread modifichino lo stesso valore nello stesso momento. Rust se ne occupa in compilazione: i dati condivisi che possono cambiare vanno avvolti in qualcosa come Arc<Mutex<T>>, altrimenti il programma non compila (vedi Stato condiviso). Nei progetti veri il crate rayon rende tutto ancora più breve, un po’ come multiprocessing.Pool.map ma con i thread:
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Anche Python sta cambiando su questo punto: Python 3.13 ha aggiunto una build «free-threaded» opzionale, senza GIL. Ma il CPython standard che la maggior parte delle persone installa e mette in produzione ha ancora il GIL, e molte estensioni in C non sono ancora pronte per il free threading.
Scrivere estensioni Python in Rust (PyO3 + maturin)
Sezione intitolata “Scrivere estensioni Python in Rust (PyO3 + maturin)”Non devi lasciare Python per usare Rust. PyO3 (in inglese) ti permette di scrivere un modulo Python in Rust, e maturin (in inglese) lo trasforma in una normale wheel che pip può installare. È così che sono fatte Polars, pydantic-core e molte altre librerie Python veloci.
Prepara un progetto (dentro un ambiente virtuale attivato):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin scrive per te il Cargo.toml e il pyproject.toml. Poi metti le tue funzioni Rust in src/lib.rs:
use pyo3::prelude::*;
/// Un modulo Python scritto in Rust.#[pymodule]mod fastmath { use pyo3::prelude::*;
/// Somma i quadrati di una lista di numeri. #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// Conta le parole in un testo. #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}Compilalo nel tuo ambiente virtuale con maturin develop (aggiungi --release quando ti interessa la velocità), e usalo da Python come qualsiasi altro modulo:
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 converte i tipi per te: una list Python di interi diventa un Vec<i64>, una str diventa un &str, e i commenti di documentazione diventano docstring Python. I tipi sbagliati diventano un normale TypeError di Python. Quando vuoi condividerlo, maturin build --release crea una wheel e maturin publish la carica su PyPI. Una buona regola: prima misura, poi sposta in Rust solo il ciclo critico. Passare da Python a Rust ha un piccolo costo, quindi una sola chiamata che fa tanto lavoro batte un milione di chiamate minuscole.
Le cose che ti faranno inciampare se vieni da Python
Sezione intitolata “Le cose che ti faranno inciampare se vieni da Python”Chiunque arrivi da Python prende le stesse buche. Conoscerle in anticipo aiuta.
L’assegnamento sposta il valore
Sezione intitolata “L’assegnamento sposta il valore”In Python, b = a ti dà due nomi per la stessa lista. In Rust, let b = a; sposta l’ownership di un Vec o di una String in b, e a non si può più usare:
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(); | ++++++++Il compilatore dice che a è stato spostato in b e poi usato di nuovo, e suggerisce di clonarlo. La soluzione è prendere in prestito (let b = &a;, se ti serve solo leggere) o fare di proposito una copia vera (a.clone()). Questa regola, l’ownership, è l’idea nuova più importante per te. Leggi con calma quella pagina e Prestito.
Il resto della lista
Sezione intitolata “Il resto della lista”- Gli interi hanno una dimensione. L’
intdi Python non va mai in overflow. Uni32di Rust si ferma a 2.147.483.647; andare oltre provoca un panic in una build di debug. Sceglii64ou64per i numeri grandi, e vedi Overflow degli interi. - Divisione e resto funzionano in modo diverso. In Rust
7 / 2vale3, perché entrambi i lati sono interi (il//di Python). Scrivi7.0 / 2.0per ottenere3.5. Inoltre-7 % 3vale-1in Rust ma2in Python; usa(-7_i32).rem_euclid(3)per avere la risposta di Python. - Niente mescolanza di tipi numerici.
let x: f64 = 2.5 * 3;non compila, perché3è un intero. Scrivi3.0, o converti conas f64(vedi Conversione di tipo). - Non puoi indicizzare una stringa.
word[0]è un errore di compilazione, perché le stringhe di Rust sono UTF-8 e un «carattere» può occupare più byte. Usaword.chars().next()oword.chars().nth(i). Vedi Stringhe. Stringcontro&str. Python ha un solo tipostr. Rust ne ha due:Stringpossiede il suo testo,&strlo prende in prestito. Di solito le funzioni accettano&stre le struct contengonoString.- Le variabili sono immutabili per default. Scrivi
let mutquando un valore deve cambiare. Se te lo dimentichi, il compilatore te lo dice. - Graffe e punti e virgola. A Rust l’indentazione non importa; le
{ }sì. E l’ultima riga di una funzione senza punto e virgola è il suo valore di ritorno:fn double(x: i32) -> i32 { x * 2 }. - Ogni
ife ognimatchdevono essere completi e con i tipi giusti. Niente valori «truthy»:if countnon compila, scriviif count > 0. Unmatchdeve coprire ogni caso. - Una HashMap non ha ordine. Il
dictdi Python ricorda l’ordine di inserimento; laHashMapdi Rust no. Ordina prima di stampare, usa unaBTreeMap, o aggiungi il crateindexmap. - C’è un passaggio di compilazione. Niente REPL, e una build pulita di un progetto con molti crate richiede un po’ di tempo. Usa
cargo checkmentre scrivi (salta la generazione del codice ed è molto più veloce), ecargo run --releasequando misuri la velocità. - Il borrow checker farà resistenza. Schemi normali in Python, come tanti oggetti che contengono e modificano un unico oggetto condiviso, o oggetti che puntano al loro genitore, richiedono smart pointer o un design diverso. Mettiti in conto qualche settimana frustrante. Poi diventa molto più facile.
Un percorso di studio per sviluppatori Python
Sezione intitolata “Un percorso di studio per sviluppatori Python”Non devi leggere questo sito in ordine. Da sviluppatore Python, questo percorso si sofferma dove Rust è più diverso da quello che conosci:
- Primi passi e Strumenti e configurazione: installa Rust ed esegui
cargo new. - Variabili, Tipi di dati e Overflow degli interi: i tipi sono la prima abitudine nuova.
- Funzioni, If…else e Cicli: letture veloci; fai caso alle espressioni e ai punti e virgola mancanti.
- Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
- Stringhe:
Stringcontro&str, e perché non puoi indicizzare una stringa. - Struct ed Enum, poi match e Pattern matching.
- Option e Result, poi Errori personalizzati.
- Vettori, HashMap e HashSet.
- Closure e Iteratori: le tue comprehension e i tuoi generatori.
- Trait, Generici, Trait comuni e Trait object.
- Moduli, Cargo e crate e Test.
- Lifetime e Smart pointer.
- Concorrenza, Stato condiviso e Async/await.
- JSON con Serde, poi costruisci un tool CLI con clap o un’API web.
Pratica
Sezione intitolata “Pratica”Entrambi gli esercizi ti danno un frammento di Python. Portalo in Rust.
1. Porta una comprehension e un dict
Sezione intitolata “1. Porta una comprehension e un dict”Porta questo codice Python. Suggerimento: per stampare i nomi in ordine, raccoglili in un Vec<&str>, chiama .sort() e poi .join(", "). Per la media, converti con as f64 prima di dividere.
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("passed:", ", ".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: {average:.1f}")Mostra la soluzione
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!("promossi: {}", 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!("media: {average:.1}");}Output:
promossi: Ada, Cy[1, 9, 25, 49, 81]media: 63.0Iterare su una HashMap ti dà coppie di riferimenti, ed è per questo che vedi **score e *name: seguono i riferimenti fino ai valori. Qui il sort() non è facoltativo: senza, i nomi potrebbero uscire in un ordine qualsiasi. E total as f64 serve perché Rust non trasforma mai un intero in un float alle tue spalle.
2. Porta try/except in Result
Sezione intitolata “2. Porta try/except in Result”Porta questo codice Python. Ogni riga dovrebbe essere nome: età. Suggerimento: line.split_once(':') restituisce un Option<(&str, &str)>; trasformalo in un Result con .ok_or(...), poi usa ?. Fai in modo che la tua funzione restituisca Result<(String, u32), String>.
def parse_line(line): if ":" not in line: raise ValueError("missing ':'") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"bad age: {e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: forty"]: try: name, age = parse_line(line) ages[name] = age print(f"ok: {name} is {age}") except ValueError as e: print(f"skipped {line!r}: {e}")print(len(ages), "saved")Mostra la soluzione
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("manca ':'")?; let age: u32 = age.trim().parse().map_err(|e| format!("età non valida: {e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: forty"] { match parse_line(line) { Ok((name, age)) => { println!("ok: {name} ha {age} anni"); ages.insert(name, age); } Err(e) => println!("saltata {line:?}: {e}"), } }
println!("salvati: {}", ages.len());}Output:
ok: Ada ha 36 annisaltata "Grace 45": manca ':'saltata "Linus: forty": età non valida: invalid digit found in stringsalvati: 1Entrambi i raise sono diventati valori Err, e il try/except è diventato un match. Il ? dopo ok_or("manca ':'") funziona anche se l’errore è un &str e la funzione restituisce errori String, perché ? converte l’errore con From. Nota l’ordine dentro Ok: prima stampiamo, poi insert sposta name nella mappa. Scambia quelle due righe e il compilatore ti dirà che name è stato spostato.
Riepilogo
Sezione intitolata “Riepilogo”- Molto di Python si porta dietro: cicli sugli iteratori, tuple, closure,
matched elaborazione dei dati in stile comprehension (come catene di iteratori). - I grandi cambiamenti sono i tipi statici controllati prima dell’esecuzione, l’ownership al posto del garbage collector,
Optional posto diNone, eResult+?al posto delle eccezioni. - Le classi diventano
struct+impl, il duck typing diventa trait, e i blocchiwithdiventano ilDropautomatico. UnaHashMapnon ha ordine, e gli interi hanno una dimensione fissa. - Rust non ha il GIL: i thread girano in parallelo, e il compilatore impedisce le data race.
- Cargo sostituisce pip, venv e compagnia;
cargo fmt,cargo clippyecargo testsostituiscono black, ruff e pytest. - Puoi usare entrambi i linguaggi insieme: PyO3 e maturin trasformano codice Rust in un modulo Python.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa usa Rust al posto del None di Python?
2. In Python, b = a dà due nomi alla stessa lista. Cosa fa let b = a; con un Vec in Rust?
3. Quanto vale 7 / 2 in Rust, se entrambi i numeri sono interi?
4. Vuoi velocizzare con Rust una funzione lenta di un progetto Python. Quali strumenti usi?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- The Rust Programming Language (in inglese): il libro ufficiale. I capitoli 4 (ownership), 6 (enum e
Option), 9 (errori) e 13 (closure e iteratori) sono i più importanti per chi viene da Python. - PyO3 user guide (in inglese): tutto su come chiamare Rust da Python (e Python da Rust).
- Maturin user guide (in inglese): compilare, testare e pubblicare pacchetti Python scritti in Rust.
- Rust by Example (in inglese): brevi esempi eseguibili, ideali se impari meglio leggendo codice.
- Are We Learning Yet? (in inglese): una panoramica dei crate di Rust per il machine learning e i dati.