Rust für Python-Entwickler: ein praktischer Leitfaden
Wenn du Python kannst, kennst du schon viele Ideen von Rust. Rust hat for-Schleifen über Iteratoren, Tupel, Closures (wie die Lambdas in Python, nur nützlicher), ein match, das ähnlich funktioniert wie match in Python, und Iterator-Ketten, die dasselbe erledigen wie List Comprehensions. Rust teilt sogar ein Stück vom Geschmack von Python: einen einzigen offensichtlichen Weg, Code zu formatieren, ein freundliches Paket-Tool und Fehlermeldungen, die sich wirklich Mühe geben, dir zu helfen.
Neu ist vor allem der Compiler. Python prüft Typen, während dein Programm läuft. Rust prüft sie alle, bevor es läuft. Python räumt Speicher mit einem Garbage Collector auf. Rust nutzt Ownership (Eigentum) und Borrowing (Ausleihen), geprüft beim Kompilieren. Python hat None und Exceptions. Rust hat Option und Result. In diese drei Dinge fließt der Großteil deiner Lernzeit.
Warum lernen Python-Entwickler also Rust? Das sind die ehrlichen Gründe:
- Geschwindigkeit. Rust wird zu nativem Maschinencode kompiliert. Rechenintensive Arbeit, die in reinem Python langsam ist (Parsen, Zahlen verarbeiten, große Schleifen), ist in Rust meist viel schneller – ganz ohne Tuning-Tricks.
- Echte Threads. Im Standard-CPython lässt der GIL immer nur einen Thread gleichzeitig Python-Code ausführen. Rust-Threads laufen gleichzeitig auf allen CPU-Kernen, und der Compiler verhindert Data Races, bevor dein Programm läuft.
- Fehler werden früher gefunden. Ein Tippfehler in einem Attributnamen, ein vergessenes
Noneoder eine Funktion, die mit dem falschen Typ aufgerufen wird: In Python merkst du das erst, wenn diese Zeile läuft. In Rust kompiliert das Programm erst, wenn alles behoben ist. - Du musst dich nicht entscheiden. Mit PyO3 (auf Englisch) und maturin schreibst du den langsamen Teil eines Python-Projekts in Rust und importierst ihn mit
importwie jedes andere Modul. Beliebte Python-Tools wie Polars, Ruff und uv sind in Rust geschrieben.
Wenn du lieber einen allgemeinen Vergleich von Rust mit Python, Go und C++ suchst, schau unter Rust vs. C++, Go und Python.
Sprachkonzepte: von Python zu Rust
Abschnitt betitelt „Sprachkonzepte: von Python zu Rust“Nutze diese Tabelle wie einen Sprachführer. Die letzte Spalte verlinkt die Lektion auf dieser Seite, die die Rust-Seite erklärt.
| Python | Rust | Mehr dazu |
|---|---|---|
| Dynamische Typen, geprüft zur Laufzeit | Statische Typen, geprüft beim Kompilieren. Rust leitet die meisten selbst ab, du schreibst also weniger Typen, als du denkst | Datentypen |
Type Hints (def f(x: int) -> str) |
Typen in Funktionssignaturen sind Pflicht, und der Compiler setzt sie durch | Funktionen |
x = 5 |
let x = 5; (unveränderbar). let mut x = 5;, wenn du den Wert ändern willst |
Variablen |
int (wächst so groß wie nötig) |
Ganzzahlen mit fester Größe: i32, i64, u64, usize, i128 … Sie können überlaufen |
Datentypen, Integer-Überlauf |
float |
f64 (oder f32) |
Datentypen |
str |
String (besessen, wachsend) und &str (eine geliehene Sicht auf Text) |
Strings |
f-Strings: f"Hi {name}" |
format!("Hi {name}") und println!("Hi {name}") |
Ausgabe |
print(repr(x)) / __repr__ |
println!("{x:?}") mit #[derive(Debug)] |
Ausgabe, Wichtige Traits |
__str__ |
Display implementieren (dann funktioniert auch .to_string()) |
Wichtige Traits |
__eq__, __hash__, __lt__ |
PartialEq, Eq, Hash, PartialOrd, Ord per derive |
Wichtige Traits |
None, Optional[T] |
Option<T>: entweder Some(value) oder None |
Option |
x or default |
x.unwrap_or(default) |
Option |
raise, try/except |
Result<T, E> + der Operator ? |
Result, Eigene Fehlertypen |
Eine Exception für einen echten Bug (assert, IndexError) |
panic! (beendet den Thread) |
Panic |
class mit __init__ und Methoden |
struct für die Daten + ein impl-Block für die Methoden; new ist eine ganz normale Funktion |
Structs |
@dataclass |
struct mit #[derive(Debug, Clone, PartialEq)] |
Structs |
self |
&self (lesen), &mut self (ändern) oder self (Ownership übernehmen) |
Structs |
| Vererbung | Keine Vererbung. Nutze Komposition (ein Struct in einem Struct) + Traits mit Standardmethoden | Traits, Entwurfsmuster |
Duck Typing, Protocol, ABC |
trait |
Traits |
| Eine Liste mit Objekten verschiedener Klassen | Vec<Box<dyn Trait>> (ein Trait-Objekt) |
Trait-Objekte |
TypeVar / generische Funktionen |
Generics mit Trait Bounds: fn largest<T: Ord>(items: &[T]) |
Generics |
Enum-Klasse |
enum, bei dem jede Variante eigene Daten tragen kann |
Enums |
match / case (ab 3.10) |
match (der Compiler prüft, ob du jeden Fall abgedeckt hast) |
Match, Pattern Matching |
if my_list: (Truthiness) |
Sei explizit: if !my_list.is_empty(). In ein if gehört nur ein bool |
If/Else |
for i in range(10) |
for i in 0..10 |
For-Schleifen |
enumerate(xs), zip(a, b) |
xs.iter().enumerate(), a.iter().zip(b.iter()) |
Iteratoren |
lambda x: x + 1 |
Closures: |x| x + 1 (und sie dürfen viele Zeilen lang sein) |
Closures |
List Comprehensions, map, filter |
Iterator-Adapter: filter, map, danach collect() |
Iteratoren |
Generatoren und yield |
Ein eigener Typ, der Iterator implementiert, oder std::iter::from_fn |
Eigene Iteratoren |
with open(...) as f: |
Nichts zu schreiben: Dateien und Locks werden geschlossen, wenn ihr Besitzer seinen Gültigkeitsbereich verlässt (Drop) |
Gültigkeitsbereich, Dateien lesen und schreiben |
Standardargumente, *args, **kwargs |
Gibt es nicht. Nutze Option-Parameter, Slices (&[T]), ein Struct mit Default oder einen Builder |
Funktionen, Entwurfsmuster |
| Decorators | Keine direkte Entsprechung. Umschließe mit Closures oder nutze Attribut-Makros (wie #[tokio::main]) |
Closures, Prozedurale Makros |
import und Module |
mod und use |
Module |
| Paket auf PyPI | Crate auf crates.io | Cargo und Crates |
if __name__ == "__main__": |
fn main() |
Erste Schritte |
threading (mit dem GIL) |
std::thread: echte parallele Threads |
Nebenläufigkeit |
multiprocessing, concurrent.futures |
Threads oder das Crate rayon für parallele Iteratoren |
Nebenläufigkeit, Geteilter Zustand |
asyncio, async def, await |
async fn und .await, mit einer Runtime wie tokio |
Async/Await |
7 // 2, 2 ** 10 |
7 / 2 (Ganzzahldivision bei Ganzzahlen), 2_i64.pow(10) |
Operatoren |
int("42"), str(42) |
"42".parse::<i32>() (gibt ein Result zurück), 42.to_string() |
Typumwandlung, Konvertierungen |
Collections
Abschnitt betitelt „Collections“Die eingebauten Collections von Python haben alle ein Zuhause in der Standardbibliothek von Rust. Die größten Unterschiede: Eine Collection in Rust enthält nur einen Typ (ein Vec<i32> enthält nur i32-Werte), und eine HashMap merkt sich die Einfügereihenfolge nicht.
| Python | Rust | Mehr dazu |
|---|---|---|
list |
Vec<T> |
Vektoren |
tuple |
Tupel (i32, String) oder ein kleines struct mit benannten Feldern |
Tupel |
dict |
HashMap<K, V> (keine feste Reihenfolge!) |
HashMap |
dict, wenn du sortierte Schlüssel brauchst |
BTreeMap<K, V> |
HashMap |
dict, wenn du die Einfügereihenfolge brauchst |
Das Crate indexmap |
|
set, frozenset |
HashSet<T>, BTreeSet<T> |
HashSet |
collections.Counter, defaultdict |
HashMap mit der Entry-API: *map.entry(k).or_insert(0) += 1 |
HashMap |
collections.deque |
VecDeque<T> |
VecDeque und BinaryHeap |
heapq (kleinstes Element zuerst) |
BinaryHeap<T> (größtes Element zuerst; mit Reverse umschlossen das kleinste zuerst) |
VecDeque und BinaryHeap |
namedtuple |
struct |
Structs |
bytes, bytearray |
&[u8], Vec<u8> |
Slices |
Slicing: xs[1:3] |
Slices: &xs[1..3] (keine negativen Indizes; nutze xs.len() - 1) |
Slices |
Werkzeuge: von pip zu cargo
Abschnitt betitelt „Werkzeuge: von pip zu cargo“Hier atmen viele Python-Entwickler ein wenig auf. Rust hat ein einziges offizielles Tool, Cargo, das die Aufgaben von pip, venv, setuptools, pytest und mehr übernimmt. Die Debatte „Welches Packaging-Tool nehmen wir?“ gibt es nicht.
| Python | Rust | Hinweise |
|---|---|---|
python-Interpreter |
rustc-Compiler, meist über cargo aufgerufen |
Du kompilierst erst und startest dann eine native Binärdatei. Siehe Erste Schritte |
| pyenv | rustup (und eine Datei rust-toolchain.toml, um eine Version festzulegen) |
Siehe Rust-Werkzeuge und Einrichtung |
| pip, poetry, uv | cargo |
Baut, startet, testet, fügt Pakete hinzu, veröffentlicht. Siehe Cargo und Crates |
pip install requests |
cargo add reqwest |
Fügt das Crate nur für dieses Projekt zur Cargo.toml hinzu |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
Wird für dich erzeugt |
| venv / virtualenv | Nicht nötig | Jedes Projekt hat seine eigenen Abhängigkeiten und baut in seinen eigenen Ordner target/ |
| PyPI | crates.io | Die API-Doku jedes veröffentlichten Crates steht auf docs.rs |
python main.py |
cargo run (cargo run --release für volle Geschwindigkeit) |
Debug-Builds laufen deutlich langsamer; Geschwindigkeit immer im Release-Modus messen |
pipx install ruff |
cargo install ripgrep |
Installiert ein Kommandozeilen-Tool |
| PyInstaller | cargo build --release |
Du bekommst eine einzige native Binärdatei; deine Nutzer brauchen kein Rust |
| black | cargo fmt |
Ein einheitlicher Stil für alle |
| ruff, pylint, flake8 | cargo clippy |
Hunderte hilfreiche Lints, offiziell |
| mypy, pyright | Der Compiler selbst | Typprüfung ist nicht optional und kein eigener Schritt |
| pytest, unittest | Eingebautes #[test] und cargo test (cargo nextest ist ein beliebter, schnellerer Runner) |
Siehe Testen |
pytest-Fixtures und parametrize |
rstest |
|
| hypothesis | proptest |
Property-based Testing. Siehe Testen für Fortgeschrittene |
unittest.mock |
mockall oder handgeschriebene Fakes, die deinen Trait implementieren |
|
| timeit, pytest-benchmark | criterion |
Siehe Performance |
| cProfile, py-spy | cargo flamegraph, perf oder ein beliebiger nativer Profiler |
|
| Docstrings + Sphinx | ///-Doc-Kommentare in Markdown + cargo doc |
Code-Beispiele in der Doku laufen als Tests. Siehe Dokumentation |
| Die Python-REPL, Jupyter | Der Rust Playground; evcxr_repl und evcxr_jupyter |
Rust hat keine eingebaute REPL. Zum Ausprobieren nimmt man meist kleine cargo run-Projekte |
| VS Code + Pylance, PyCharm | VS Code + rust-analyzer oder JetBrains RustRover | rust-analyzer bietet Vervollständigung, Sprung zur Definition und Fehler direkt im Code |
Hier ist eine kleine Cargo.toml, damit du sie mit einer pyproject.toml vergleichen kannst:
[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"] }Bibliotheken und Frameworks: „Was nehmen wir statt …?“
Abschnitt betitelt „Bibliotheken und Frameworks: „Was nehmen wir statt …?““„Ich nutze requests, FastAPI und pandas. Was gibt es dafür in Rust?“ Hier sind die nächsten Entsprechungen. Das Ökosystem von Python ist viel älter und größer. In manchen Bereichen (vor allem Data Science) hat Rust deshalb weniger und jüngere Optionen.
Web und Netzwerk
Abschnitt betitelt „Web und Netzwerk“| Python | Rust | Hinweise |
|---|---|---|
| Flask, FastAPI | axum (wir bauen eine API in Projekt: Web-API mit Axum), actix-web | Handler sind einfache async fns; Request- und Response-Typen kommen aus deinen Structs, ein bisschen wie FastAPI mit pydantic |
| Django | Keine vollständige Entsprechung. Loco (loco-rs) ist ein Framework mit „Batterien inklusive“; die meisten Teams kombinieren axum + ein ORM + eine Template-Engine |
Keine eingebaute Admin-Oberfläche |
| requests, httpx | reqwest (async, mit optionalem blockierendem Client), ureq (einfach und blockierend) |
Siehe Netzwerkprogrammierung |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja (vom Autor von Jinja selbst), tera, askama (Templates, die beim Kompilieren geprüft werden) |
|
| BeautifulSoup | scraper |
Daten und Datenbanken
Abschnitt betitelt „Daten und Datenbanken“| Python | Rust | Hinweise |
|---|---|---|
| SQLAlchemy ORM | Diesel (Abfragen werden beim Kompilieren typgeprüft), SeaORM (async, im Entity-Stil) | Datenbanken in Rust zeigt beide mit funktionierendem Code |
| psycopg, rohes SQL | sqlx: einfaches SQL, beim Kompilieren gegen deine echte Datenbank geprüft | Async, sehr beliebt, kein ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | apalis für Job-Queues; tokio::spawn für Hintergrund-Tasks im selben Prozess |
Data Science und Machine Learning
Abschnitt betitelt „Data Science und Machine Learning“| Python | Rust | Hinweise |
|---|---|---|
| pandas | Polars | In Rust geschrieben; die Python-API, die du vielleicht schon kennst, baut auf derselben Rust-Bibliothek auf |
| NumPy | ndarray |
N-dimensionale Arrays mit Slicing und Broadcasting; deutlich weniger Extras als NumPy |
| scikit-learn | linfa, smartcore |
Weniger Algorithmen und viel kleinere Communitys |
| PyTorch | candle (Crate candle-core), burn |
Gut, um Modelle auszuführen; trainiert wird meist noch in Python |
| matplotlib | plotters |
|
Modul csv |
Crate csv + serde |
Bausteine für den Alltag
Abschnitt betitelt „Bausteine für den Alltag“| Python | Rust | Hinweise |
|---|---|---|
json |
serde + serde_json |
Der Standard; kann auch TOML, YAML und mehr. Siehe JSON mit Serde |
| pydantic | serde zum Parsen + validator oder garde für Validierungsregeln |
Dein Struct ist das Schema |
| argparse, click, typer | clap |
Du definierst deine Argumente als Struct. Siehe Projekt: CLI-Tool mit clap |
logging |
tracing oder log + env_logger |
Siehe Logging |
re |
regex |
Kein Look-around und keine Rückverweise, dafür garantiert lineare Laufzeit; fancy-regex kann beides. Siehe Reguläre Ausdrücke |
datetime |
chrono oder jiff |
Siehe Datum und Uhrzeit |
random |
rand |
Siehe Zufallszahlen |
asyncio |
tokio |
Siehe Async/Await |
multiprocessing.Pool.map |
rayon: aus .iter() wird .par_iter() |
|
itertools |
Das meiste ist schon in Iterator eingebaut; das Crate itertools ergänzt den Rest |
|
os, pathlib, subprocess |
std::env, std::path, std::fs, std::process |
Siehe Dateien lesen und schreiben, Umgebung und Prozesse |
Im direkten Vergleich: aus einer Klasse wird struct + impl
Abschnitt betitelt „Im direkten Vergleich: aus einer Klasse wird struct + impl“Hier ist eine kleine Python-Klasse:
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} hat {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)Und dasselbe in Rust:
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` ist nur eine Konvention, kein Schlüsselwort. Es gibt keine // Standardargumente, also übergibt der Aufrufer immer beide Werte. fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self`: Diese Methode verändert das Konto. fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// Das ist das __str__ von Rust.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} hat {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}Ausgabe:
Ada hat 150Ein paar Dinge sind anders. Die Felder werden vorab mit ihren Typen deklariert. Du kannst also später kein neues Attribut hinzufügen, so wie Python dir account.nickname = "A" erlaubt. Daten (das struct) und Verhalten (das impl) stehen in getrennten Blöcken. Jede Methode sagt, ob sie den Wert nur liest (&self) oder verändert (&mut self). Und weil deposit das Konto verändert, muss die Variable mit let mut angelegt sein. Rust hat keine Standardargumente. Wenn du ein Konto willst, das bei null startet, schreibst du eine zweite Funktion, etwa fn empty(owner: &str) -> Self.
Im direkten Vergleich: aus Duck Typing werden Traits
Abschnitt betitelt „Im direkten Vergleich: aus Duck Typing werden Traits“Python ist egal, von welcher Klasse ein Objekt ist. Wichtig ist nur, dass es die richtige Methode hat:
class Duck: def speak(self): return "Quak"
class Robot: def speak(self): return "Piep piep"
def introduce(thing): print(f"Es sagt: {thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError, aber erst, wenn diese Zeile läuftIn Rust wird „hat eine Methode speak“ als Trait aufgeschrieben, und jeder Typ macht mit einem impl-Block mit:
trait Speak { fn speak(&self) -> String;}
struct Duck;struct Robot;
impl Speak for Duck { fn speak(&self) -> String { "Quak".to_string() }}
impl Speak for Robot { fn speak(&self) -> String { "Piep piep".to_string() }}
// Nimmt jeden Typ an, der Speak implementiert.fn introduce(thing: &dyn Speak) { println!("Es sagt: {}", 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); // Compilerfehler: `{integer}` implementiert Speak nicht}Ausgabe:
Es sagt: QuakEs sagt: Piep piepIm Grunde ist das immer noch Duck Typing, nur vom Compiler geprüft: introduce(&42) ist ein Fehler, bevor das Programm überhaupt läuft – und kein AttributeError in Produktion. Wenn du typing.Protocol kennst, fühlen sich Traits ganz natürlich an. Eine Liste mit gemischten Typen braucht Box<dyn Speak>, weil Rust wissen muss, wie es die Methode für jedes Element aufruft (siehe Trait-Objekte). Und du kannst deinen eigenen Trait auch für Typen implementieren, die du nicht geschrieben hast, etwa String oder i32.
Im direkten Vergleich: aus None wird Option
Abschnitt betitelt „Im direkten Vergleich: aus None wird Option“In Python kann jeder Wert heimlich None sein, und du merkst es erst, wenn du ihn benutzt:
def find_email(user_id): return emails.get(user_id) # gibt None zurück, wenn der Eintrag fehlt
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'In Rust hat ein Wert, der fehlen könnte, den Typ Option<T>. Du kannst ihn erst wie einen normalen Wert benutzen, wenn du den Fall None behandelt hast:
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}: keine E-Mail hinterlegt"), } }
// Wie `find_email(3) or "unbekannt"` in Python. let email = find_email(3).unwrap_or("unbekannt".to_string()); println!("{email}");
// Den Wert darin verändern, falls es einen gibt. let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}Ausgabe:
3: keine E-Mail hinterlegtunbekanntSome("[email protected]")Der Rust-Compiler lässt dich .to_uppercase() nicht auf einer Option<String> aufrufen. Der Fehler „'NoneType' object has no attribute“ kann also einfach nicht passieren. Ein String in Rust ist immer ein echter String. Die Hilfsmethoden lernst du auf der Seite Option kennen, die Kurzform if let Some(email) = ... unter If Let / While Let.
Im direkten Vergleich: aus Exceptions werden Result und ?
Abschnitt betitelt „Im direkten Vergleich: aus Exceptions werden Result und ?“In Python wirft int() eine Exception, und du fängst sie irgendwo weiter oben im Call Stack ab:
def parse_port(text): port = int(text) # kann ValueError werfen if not 1 <= port <= 65535: raise ValueError(f"Port {port} liegt außerhalb des gültigen Bereichs") return port
for text in ["8080", "http", "70000"]: try: print(f"Port {parse_port(text)}") except ValueError as e: print(f"Fehler: {e}")In Rust sagt eine Funktion, die fehlschlagen kann, das in ihrem Rückgabetyp. Der Operator ? bedeutet: „Wenn das fehlgeschlagen ist, gib den Fehler sofort an meinen Aufrufer zurück.“ Er erledigt dieselbe Aufgabe, wie eine Exception nach oben durchzureichen – nur siehst du es im Code:
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} ist keine Zahl ({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("Port {port} liegt außerhalb des gültigen Bereichs")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("Port {port}"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
Port 8080Fehler: "http" ist keine Zahl (invalid digit found in string)Fehler: Port 70000 liegt außerhalb des gültigen BereichsDie Meldung „invalid digit found in string“ kommt von Rust selbst und ist deshalb auf Englisch. Sie bedeutet: „ungültige Ziffer im String gefunden“.
Der große Unterschied: In Python kann jede Funktion eine Exception werfen, und nichts in ihrer Signatur verrät es dir. In Rust ist Result Teil der Signatur. Aufrufer können den Fehler also nicht vergessen. Beim Lernen ist String als Fehlertyp völlig in Ordnung. Echte Projekte nutzen eigene Fehler-Enums (Eigene Fehlertypen) oder das Crate anyhow, das sich sehr nach „irgendeine Exception werfen“ anfühlt (anyhow und thiserror).
Im direkten Vergleich: aus Comprehensions und Dicts werden Iteratoren und HashMap
Abschnitt betitelt „Im direkten Vergleich: aus Comprehensions und Dicts werden Iteratoren und HashMap“List Comprehensions und Wörter zählen mit einem Dict sind Python-Alltag:
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 wird aus einer Comprehension eine Iterator-Kette, die mit collect() endet, und aus dem Dict wird eine 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; }
// Häufigste zuerst; bei Gleichstand alphabetisch. 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}"); }}Ausgabe:
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1Der englische Beispielsatz bleibt hier absichtlich auf Englisch: Das Programm filtert nach der Wortlänge in Bytes und sortiert alphabetisch, und mit deutschen Wörtern (und Umlauten) käme etwas anderes heraus.
Die Kette liest sich von links nach rechts statt von innen nach außen, aber die Idee ist dieselbe. Wie ein Generator-Ausdruck ist sie lazy (sie arbeitet erst bei Bedarf): Nichts passiert, bis collect(), sum(), count() oder eine for-Schleife die Werte durchzieht. collect() muss wissen, was es bauen soll. Deshalb hat loud den Typ Vec<String>. Und achte auf den Sortierschritt: Ein dict in Python merkt sich die Einfügereihenfolge, eine HashMap in Rust nicht. Deshalb sortieren wir immer vor der Ausgabe. Mehr zum Sortieren steht auf der Seite Sortieren und Suchen.
Im direkten Vergleich: der GIL vs. echte Threads
Abschnitt betitelt „Im direkten Vergleich: der GIL vs. echte Threads“Im Standard-CPython lässt der Global Interpreter Lock (GIL) immer nur einen Thread gleichzeitig Python-Bytecode ausführen. Threads helfen, wenn du auf das Netzwerk wartest. Rechenintensive Arbeit wie diese wird mit mehr Threads aber nicht schneller. Python-Entwickler greifen deshalb zu 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("insgesamt:", sum(counts))Rust hat keinen GIL. Jedes thread::spawn ist ein echter Thread des Betriebssystems, und die Threads laufen gleichzeitig auf verschiedenen CPU-Kernen:
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)];
// Jeder Bereich bekommt seinen eigenen OS-Thread. Sie laufen wirklich gleichzeitig. 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} Primzahlen"); } println!("insgesamt: {}", counts.iter().sum::<usize>());}Ausgabe:
0..25000: 2762 Primzahlen25000..50000: 2371 Primzahlen50000..75000: 2260 Primzahlen75000..100000: 2199 Primzahleninsgesamt: 9592thread::scope wartet auf alle Threads, bevor es zurückkehrt. Deshalb dürfen die Threads lokale Daten gefahrlos ausleihen. Ohne GIL machst du dir vielleicht Sorgen, dass zwei Threads gleichzeitig denselben Wert ändern. Rust kümmert sich darum beim Kompilieren: Geteilte Daten, die sich ändern können, müssen in etwas wie Arc<Mutex<T>> stecken, sonst kompiliert das Programm nicht (siehe Geteilter Zustand). In echten Projekten macht das Crate rayon das noch kürzer, ein bisschen wie multiprocessing.Pool.map, nur mit Threads:
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Auch Python ändert sich hier: Python 3.13 hat einen optionalen „free-threaded“ Build ohne GIL eingeführt. Das Standard-CPython, das die meisten installieren und ausliefern, hat aber weiterhin den GIL, und viele C-Erweiterungen sind noch nicht bereit für Free Threading.
Python-Erweiterungen in Rust schreiben (PyO3 + maturin)
Abschnitt betitelt „Python-Erweiterungen in Rust schreiben (PyO3 + maturin)“Du musst Python nicht verlassen, um Rust zu nutzen. Mit PyO3 (auf Englisch) schreibst du ein Python-Modul in Rust, und maturin (auf Englisch) baut daraus ein normales Wheel, das pip installieren kann. So entstehen Polars, pydantic-core und viele andere schnelle Python-Bibliotheken.
Leg ein Projekt an (in einer aktivierten virtuellen Umgebung):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin schreibt die Cargo.toml und die pyproject.toml für dich. Danach kommen deine Rust-Funktionen in src/lib.rs:
use pyo3::prelude::*;
/// Ein Python-Modul, geschrieben in Rust.#[pymodule]mod fastmath { use pyo3::prelude::*;
/// Addiert die Quadrate einer Liste von Zahlen. #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// Zählt die Wörter in einem Text. #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}Bau es mit maturin develop in deine virtuelle Umgebung (mit --release, wenn dir Geschwindigkeit wichtig ist), und nutze es aus Python wie jedes andere Modul:
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 wandelt die Typen für dich um: Aus einer Python-list mit Ganzzahlen wird ein Vec<i64>, aus einem str ein &str, und aus den Doc-Kommentaren werden Python-Docstrings. Falsche Typen werden zu einem ganz normalen TypeError in Python. Wenn du es weitergeben willst, erzeugt maturin build --release ein Wheel, und maturin publish lädt es auf PyPI hoch. Eine gute Regel: Erst messen, dann nur die heiße Schleife nach Rust verschieben. Jeder Wechsel zwischen Python und Rust kostet ein wenig. Ein Aufruf, der viel Arbeit erledigt, ist deshalb besser als eine Million winziger Aufrufe.
Stolpersteine für Python-Entwickler
Abschnitt betitelt „Stolpersteine für Python-Entwickler“Alle, die von Python kommen, stolpern über dieselben Dinge. Wenn du sie vorher kennst, hilft das.
Eine Zuweisung verschiebt den Wert
Abschnitt betitelt „Eine Zuweisung verschiebt den Wert“In Python bekommst du mit b = a zwei Namen für dieselbe Liste. In Rust verschiebt let b = a; das Ownership an einem Vec oder String nach b (ein Move), und a ist danach nicht mehr benutzbar:
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(); | ++++++++Der Compiler sagt auf Englisch: a wird ausgeliehen, nachdem sein Wert schon verschoben wurde. Er schlägt auch gleich vor, den Wert zu klonen. Die Lösung ist, auszuleihen (let b = &a;, wenn du nur lesen musst) oder bewusst eine echte Kopie anzulegen (a.clone()). Diese eine Regel, Ownership, ist die wichtigste neue Idee für dich. Lies diese Seite und Borrowing in Ruhe.
Der Rest der Liste
Abschnitt betitelt „Der Rest der Liste“- Ganzzahlen haben eine Größe. Ein
intin Python läuft nie über. Eini32in Rust endet bei 2.147.483.647; wer darüber hinausgeht, löst in einem Debug-Build eine Panic aus. Nimmi64oderu64für große Zahlen und lies Integer-Überlauf. - Division und Rest funktionieren anders.
7 / 2ergibt in Rust3, weil beide Seiten Ganzzahlen sind (wie//in Python). Schreib7.0 / 2.0, um3.5zu bekommen. Außerdem ist-7 % 3in Rust-1, in Python aber2. Mit(-7_i32).rem_euclid(3)bekommst du das Ergebnis von Python. - Keine gemischten Zahlentypen.
let x: f64 = 2.5 * 3;kompiliert nicht, weil3eine Ganzzahl ist. Schreib3.0oder wandle mitas f64um (siehe Typumwandlung). - Du kannst einen String nicht indizieren.
word[0]ist ein Compilerfehler, weil Strings in Rust UTF-8 sind und ein „Zeichen“ mehrere Bytes lang sein kann. Nutzeword.chars().next()oderword.chars().nth(i). Siehe Strings. Stringvs.&str. Python hat einen einzigenstr-Typ. Rust hat zwei:Stringbesitzt seinen Text,&strleiht ihn aus. Funktionen nehmen meist&strentgegen, und Structs speichern meistString.- Variablen sind standardmäßig unveränderbar. Schreib
let mut, wenn sich ein Wert ändern soll. Der Compiler sagt dir, wenn du es vergessen hast. - Klammern und Semikolons. Einrückung ist Rust egal,
{ }nicht. Und die letzte Zeile einer Funktion ohne Semikolon ist ihr Rückgabewert:fn double(x: i32) -> i32 { x * 2 }. - Jedes
ifund jedesmatchmuss vollständig und typkorrekt sein. Keine Truthiness:if countkompiliert nicht, du schreibstif count > 0. Einmatchmuss jeden Fall abdecken. - Eine HashMap hat keine Reihenfolge. Ein
dictin Python merkt sich die Einfügereihenfolge, eineHashMapin Rust nicht. Sortiere vor der Ausgabe, nimm eineBTreeMapoder füge das Crateindexmaphinzu. - Es gibt einen Kompilierschritt. Keine REPL, und ein kompletter Build eines Projekts mit vielen Crates dauert eine Weile. Nutze beim Bearbeiten
cargo check(das überspringt die Codeerzeugung und ist viel schneller) undcargo run --release, wenn du Geschwindigkeit misst. - Der Borrow Checker hält dagegen. Muster, die in Python ganz normal sind, etwa viele Objekte, die alle ein gemeinsames Objekt halten und verändern, oder Objekte, die auf ihr Elternobjekt zurückzeigen, brauchen Smart Pointer oder ein anderes Design. Rechne mit ein paar frustrierenden Wochen. Danach wird es viel leichter.
Ein Lernpfad für Python-Entwickler
Abschnitt betitelt „Ein Lernpfad für Python-Entwickler“Du musst diese Seite nicht der Reihe nach lesen. Als Python-Entwickler nimmst du dir auf diesem Weg Zeit für das, was in Rust am meisten anders ist als das, was du kennst:
- Erste Schritte mit Rust und Rust-Werkzeuge und Einrichtung: Rust installieren und
cargo newausführen. - Variablen, Datentypen und Integer-Überlauf: Typen sind die erste neue Gewohnheit.
- Funktionen, If/Else und Schleifen: schnell gelesen; achte auf Ausdrücke und fehlende Semikolons.
- Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
- Strings:
Stringvs.&str, und warum du einen String nicht indizieren kannst. - Structs und Enums, dann Match und Pattern Matching.
- Option und Result, dann Eigene Fehlertypen.
- Vektoren, HashMap und HashSet.
- Closures und Iteratoren: deine Comprehensions und Generatoren.
- Traits, Generics, Wichtige Traits und Trait-Objekte.
- Module, Cargo und Crates und Testen.
- Lifetimes und Smart Pointer.
- Nebenläufigkeit, Geteilter Zustand und Async/Await.
- JSON mit Serde, danach baust du eine CLI mit clap oder eine Web-API.
Übungen
Abschnitt betitelt „Übungen“Beide Übungen geben dir ein Stück Python-Code. Portiere es nach Rust.
1. Eine Comprehension und ein Dict portieren
Abschnitt betitelt „1. Eine Comprehension und ein Dict portieren“Portiere diesen Python-Code. Tipp: Um die Namen sortiert auszugeben, sammle sie in einen Vec<&str>, ruf .sort() auf und danach .join(", "). Für den Durchschnitt wandelst du vor dem Teilen mit as f64 um.
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("bestanden:", ", ".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"Durchschnitt: {average:.1f}")Lösung anzeigen
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!("bestanden: {}", 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!("Durchschnitt: {average:.1}");}Ausgabe:
bestanden: Ada, Cy[1, 9, 25, 49, 81]Durchschnitt: 63.0Wenn du über eine HashMap iterierst, bekommst du Paare von Referenzen. Deshalb siehst du **score und *name: Sie folgen den Referenzen zurück zu den Werten. Das sort() ist hier nicht optional: Ohne es könnten die Namen in beliebiger Reihenfolge herauskommen. Und total as f64 ist nötig, weil Rust eine Ganzzahl nie hinter deinem Rücken in eine Gleitkommazahl verwandelt.
2. try/except zu Result portieren
Abschnitt betitelt „2. try/except zu Result portieren“Portiere diesen Python-Code. Jede Zeile soll die Form Name: Alter haben. Tipp: line.split_once(':') gibt eine Option<(&str, &str)> zurück. Mach daraus mit .ok_or(...) ein Result und nutze dann ?. Deine Funktion soll Result<(String, u32), String> zurückgeben.
def parse_line(line): if ":" not in line: raise ValueError("':' fehlt") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"ungültiges Alter: {e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: vierzig"]: try: name, age = parse_line(line) ages[name] = age print(f"ok: {name} ist {age}") except ValueError as e: print(f"übersprungen {line!r}: {e}")print(len(ages), "gespeichert")Lösung anzeigen
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("':' fehlt")?; let age: u32 = age.trim().parse().map_err(|e| format!("ungültiges Alter: {e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: vierzig"] { match parse_line(line) { Ok((name, age)) => { println!("ok: {name} ist {age}"); ages.insert(name, age); } Err(e) => println!("übersprungen {line:?}: {e}"), } }
println!("{} gespeichert", ages.len());}Ausgabe:
ok: Ada ist 36übersprungen "Grace 45": ':' fehltübersprungen "Linus: vierzig": ungültiges Alter: invalid digit found in string1 gespeichertAus beiden raise-Anweisungen wurden Err-Werte, und aus try/except wurde ein match. Das ? nach ok_or("':' fehlt") funktioniert, obwohl der Fehler ein &str ist und die Funktion String-Fehler zurückgibt, weil ? den Fehler mit From umwandelt. Achte auf die Reihenfolge innerhalb von Ok: Wir geben zuerst aus, danach verschiebt insert den name in die Map. Vertausch die beiden Zeilen, und der Compiler sagt dir, dass name verschoben wurde.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Vieles aus Python nimmst du mit: Schleifen über Iteratoren, Tupel, Closures,
matchund Datenverarbeitung im Stil von Comprehensions (als Iterator-Ketten). - Die großen Umstellungen sind statische Typen, die vor dem Ausführen geprüft werden, Ownership statt Garbage Collector,
OptionstattNoneundResult+?statt Exceptions. - Aus Klassen werden
struct+impl, aus Duck Typing werden Traits, und auswith-Blöcken wird das automatischeDrop. EineHashMaphat keine Reihenfolge, und Ganzzahlen haben eine feste Größe. - Rust hat keinen GIL: Threads laufen parallel, und der Compiler verhindert Data Races.
- Cargo ersetzt pip, venv und Co.;
cargo fmt,cargo clippyundcargo testersetzen black, ruff und pytest. - Du kannst beide Sprachen mischen: PyO3 und maturin machen aus Rust-Code ein Python-Modul.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was nutzt Rust statt None aus Python?
2. In Python gibt dir b = a zwei Namen für dieselbe Liste. Was macht let b = a; mit einem Vec in Rust?
3. Was ergibt 7 / 2 in Rust, wenn beide Zahlen Ganzzahlen sind?
4. Du willst eine langsame Funktion in einem Python-Projekt mit Rust beschleunigen. Welche Tools nutzt du?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Die Programmiersprache Rust – Das Rust-Buch: die deutsche Übersetzung des offiziellen Buchs. Für Python-Entwickler sind die Kapitel 4 (Ownership), 6 (Enums und
Option), 9 (Fehlerbehandlung) und 13 (Closures und Iteratoren) am wichtigsten. - PyO3 User Guide (auf Englisch): alles darüber, wie du Rust aus Python aufrufst (und Python aus Rust).
- Maturin User Guide (auf Englisch): Python-Pakete mit Rust bauen, testen und veröffentlichen.
- Rust by Example (auf Englisch): kurze, ausführbare Beispiele – gut, wenn du am besten lernst, indem du Code liest.
- Are We Learning Yet? (auf Englisch): eine Übersicht über die Machine-Learning- und Daten-Crates von Rust.