Rust cho lập trình viên Python: hướng dẫn thực hành
Nếu bạn biết Python, bạn đã biết khá nhiều ý tưởng của Rust rồi. Rust có vòng lặp for chạy trên iterator, có tuple, có closure (giống lambda của Python nhưng hữu ích hơn nhiều), có câu lệnh match hoạt động giống match của Python, và có chuỗi iterator làm đúng việc mà list comprehension vẫn làm. Rust thậm chí còn “mượn” vài sở thích của Python: một cách định dạng code hiển nhiên duy nhất, một công cụ quản lý gói thân thiện, và thông báo lỗi cố gắng hết sức để giúp bạn.
Cái mới chủ yếu nằm ở trình biên dịch. Python kiểm tra kiểu trong lúc chương trình chạy; Rust kiểm tra tất cả trước khi chạy. Python dọn dẹp bộ nhớ bằng bộ thu gom rác (garbage collector); Rust dùng quyền sở hữu (ownership) và mượn (borrowing), được kiểm tra lúc biên dịch. Python có None và exception; Rust có Option và Result. Ba điều này là nơi bạn sẽ tốn phần lớn thời gian học.
Vậy tại sao lập trình viên Python lại học Rust? Nói thật thì có mấy lý do sau:
- Tốc độ. Rust biên dịch thành mã máy native. Những việc nặng về CPU mà Python thuần chạy chậm (parse dữ liệu, tính toán số, vòng lặp lớn) thường nhanh hơn rất nhiều trong Rust, mà không cần mẹo tối ưu nào.
- Luồng thật. Trong CPython tiêu chuẩn, GIL chỉ cho một luồng chạy code Python tại một thời điểm. Luồng (thread) của Rust chạy trên mọi lõi CPU cùng lúc, và trình biên dịch chặn data race trước khi chương trình chạy.
- Bắt bug sớm hơn. Gõ sai tên thuộc tính, quên một giá trị
None, hay gọi hàm với sai kiểu: trong Python, bạn chỉ phát hiện khi dòng đó chạy. Trong Rust, chương trình không biên dịch cho tới khi bạn sửa xong. - Bạn không phải chọn một trong hai. Với PyO3 (tiếng Anh) và maturin, bạn có thể viết phần chậm của một dự án Python bằng Rust rồi
importnó từ Python như mọi module khác. Những công cụ Python nổi tiếng như Polars, Ruff và uv đều được viết bằng Rust.
Nếu bạn muốn một bài so sánh rộng hơn giữa Rust với Python, Go và C++, hãy xem Rust so với C++, Go và Python.
Khái niệm ngôn ngữ: từ Python sang Rust
Phần tiêu đề “Khái niệm ngôn ngữ: từ Python sang Rust”Hãy dùng bảng này như một cuốn sổ tay hội thoại. Cột cuối dẫn đến bài học trên trang này giải thích phía Rust.
| Python | Rust | Tìm hiểu thêm |
|---|---|---|
| Kiểu động, kiểm tra lúc chạy | Kiểu tĩnh, kiểm tra lúc biên dịch. Rust tự suy luận phần lớn kiểu, nên bạn viết ít kiểu hơn bạn nghĩ | Kiểu dữ liệu |
Type hint (def f(x: int) -> str) |
Kiểu trong chữ ký hàm là bắt buộc, và trình biên dịch kiểm tra chúng | Hàm |
x = 5 |
let x = 5; (không đổi được). let mut x = 5; nếu bạn muốn thay đổi |
Biến |
int (lớn bao nhiêu cũng được) |
Số nguyên có kích thước cố định: i32, i64, u64, usize, i128… Chúng có thể bị tràn |
Kiểu dữ liệu, Tràn số nguyên |
float |
f64 (hoặc f32) |
Kiểu dữ liệu |
str |
String (sở hữu, có thể nới rộng) và &str (một góc nhìn mượn vào văn bản) |
Chuỗi |
f-string: f"Hi {name}" |
format!("Hi {name}") và println!("Hi {name}") |
In ra màn hình |
print(repr(x)) / __repr__ |
println!("{x:?}") với #[derive(Debug)] |
In ra màn hình, Các trait thông dụng |
__str__ |
Triển khai Display (rồi .to_string() cũng dùng được) |
Các trait thông dụng |
__eq__, __hash__, __lt__ |
Derive PartialEq, Eq, Hash, PartialOrd, Ord |
Các trait thông dụng |
None, Optional[T] |
Option<T>: hoặc Some(value) hoặc None |
Option |
x or default |
x.unwrap_or(default) |
Option |
raise, try/except |
Result<T, E> + toán tử ? |
Result, Lỗi tự định nghĩa |
Exception cho một bug thật sự (assert, IndexError) |
panic! (dừng luồng hiện tại) |
Panic |
class với __init__ và các phương thức |
struct cho dữ liệu + một khối impl cho các phương thức; new là một hàm bình thường |
Struct |
@dataclass |
struct với #[derive(Debug, Clone, PartialEq)] |
Struct |
self |
&self (đọc), &mut self (thay đổi), hoặc self (lấy quyền sở hữu) |
Struct |
| Kế thừa | Không có kế thừa. Dùng composition (struct nằm trong struct) + trait có phương thức mặc định | Trait, Mẫu thiết kế |
Duck typing, Protocol, ABC |
trait |
Trait |
| Một list chứa object của nhiều class khác nhau | Vec<Box<dyn Trait>> (một trait object) |
Trait object |
TypeVar / hàm generic |
Generic với ràng buộc trait (trait bound): fn largest<T: Ord>(items: &[T]) |
Generic |
Class Enum |
enum, mà mỗi biến thể (variant) có thể mang dữ liệu riêng |
Enum |
match / case (3.10+) |
match (trình biên dịch kiểm tra bạn đã xử lý mọi trường hợp) |
match, So khớp mẫu |
if my_list: (truthiness) |
Viết rõ ràng: if !my_list.is_empty(). Chỉ bool mới được đặt trong if |
If…else |
for i in range(10) |
for i in 0..10 |
Vòng lặp for |
enumerate(xs), zip(a, b) |
xs.iter().enumerate(), a.iter().zip(b.iter()) |
Iterator |
lambda x: x + 1 |
Closure: |x| x + 1 (và có thể dài nhiều dòng) |
Closure |
List comprehension, map, filter |
Iterator adapter: filter, map, rồi collect() |
Iterator |
Generator và yield |
Kiểu của riêng bạn triển khai Iterator, hoặc std::iter::from_fn |
Iterator tự định nghĩa |
with open(...) as f: |
Không cần viết gì: file và khóa tự đóng khi chủ sở hữu ra khỏi phạm vi (Drop) |
Phạm vi, Đọc ghi file |
Tham số mặc định, *args, **kwargs |
Không hỗ trợ. Dùng tham số Option, slice (&[T]), một struct có Default, hoặc builder |
Hàm, Mẫu thiết kế |
| Decorator | Không có thứ tương đương trực tiếp. Bọc bằng closure, hoặc dùng attribute macro (như #[tokio::main]) |
Closure, Procedural macro |
import và module |
mod và use |
Module |
| Gói trên PyPI | Crate trên crates.io | Cargo và crate |
if __name__ == "__main__": |
fn main() |
Bắt đầu với Rust |
threading (có GIL) |
std::thread: luồng song song thật sự |
Lập trình đồng thời |
multiprocessing, concurrent.futures |
Luồng, hoặc crate rayon cho iterator song song |
Lập trình đồng thời, Trạng thái dùng chung |
asyncio, async def, await |
async fn và .await, với một runtime như tokio |
Async/Await |
7 // 2, 2 ** 10 |
7 / 2 (chia nguyên khi cả hai là số nguyên), 2_i64.pow(10) |
Toán tử |
int("42"), str(42) |
"42".parse::<i32>() (trả về một Result), 42.to_string() |
Ép kiểu, Chuyển đổi |
Collection
Phần tiêu đề “Collection”Mọi collection có sẵn của Python đều có chỗ trong thư viện chuẩn của Rust. Khác biệt lớn nhất: một collection của Rust chỉ chứa một kiểu (một Vec<i32> chỉ chứa i32), và HashMap không nhớ thứ tự thêm vào.
| Python | Rust | Tìm hiểu thêm |
|---|---|---|
list |
Vec<T> |
Vector |
tuple |
Tuple (i32, String), hoặc một struct nhỏ có field đặt tên |
Tuple |
dict |
HashMap<K, V> (không có thứ tự cố định!) |
HashMap |
dict khi bạn cần khóa được sắp xếp |
BTreeMap<K, V> |
HashMap |
dict khi bạn cần giữ thứ tự thêm vào |
Crate indexmap |
|
set, frozenset |
HashSet<T>, BTreeSet<T> |
HashSet và BTreeMap |
collections.Counter, defaultdict |
HashMap với entry API: *map.entry(k).or_insert(0) += 1 |
HashMap |
collections.deque |
VecDeque<T> |
VecDeque và BinaryHeap |
heapq (nhỏ nhất ra trước) |
BinaryHeap<T> (lớn nhất ra trước; bọc trong Reverse để nhỏ nhất ra trước) |
VecDeque và BinaryHeap |
namedtuple |
struct |
Struct |
bytes, bytearray |
&[u8], Vec<u8> |
Slice |
Cắt lát: xs[1:3] |
Slice: &xs[1..3] (không có chỉ số âm; dùng xs.len() - 1) |
Slice |
Công cụ: từ pip sang cargo
Phần tiêu đề “Công cụ: từ pip sang cargo”Đây là phần mà nhiều lập trình viên Python thở phào. Rust có một công cụ chính thức, Cargo, làm việc của pip, venv, setuptools, pytest và hơn thế nữa. Không có cuộc tranh luận “nên dùng công cụ đóng gói nào?”.
| Python | Rust | Ghi chú |
|---|---|---|
Trình thông dịch python |
Trình biên dịch rustc, thường được gọi qua cargo |
Bạn biên dịch trước, rồi chạy một file binary native. Xem Bắt đầu với Rust |
| pyenv | rustup (và một file rust-toolchain.toml để ghim phiên bản) |
Xem Công cụ và cài đặt |
| pip, poetry, uv | cargo |
Build, chạy, test, thêm gói, publish. Xem Cargo và crate |
pip install requests |
cargo add reqwest |
Thêm crate vào Cargo.toml chỉ cho dự án này |
pyproject.toml / requirements.txt |
Cargo.toml |
|
poetry.lock / uv.lock |
Cargo.lock |
Được tạo tự động cho bạn |
| venv / virtualenv | Không cần | Mỗi dự án có phụ thuộc riêng và build vào thư mục target/ riêng |
| PyPI | crates.io | Tài liệu API của mọi crate đã publish đều có trên docs.rs (tiếng Anh) |
python main.py |
cargo run (cargo run --release để chạy hết tốc độ) |
Bản build debug chạy chậm hơn nhiều; hãy đo tốc độ bằng bản release |
pipx install ruff |
cargo install ripgrep |
Cài một công cụ dòng lệnh |
| PyInstaller | cargo build --release |
Bạn có một file binary native; người dùng không cần cài Rust |
| black | cargo fmt |
Một phong cách chuẩn duy nhất cho mọi người |
| ruff, pylint, flake8 | cargo clippy |
Hàng trăm lint hữu ích, chính thức |
| mypy, pyright | Chính trình biên dịch | Kiểm tra kiểu không phải tùy chọn và cũng không phải một bước riêng |
| pytest, unittest | #[test] có sẵn và cargo test (cargo nextest là trình chạy nhanh hơn được nhiều người dùng) |
Xem Kiểm thử |
Fixture và parametrize của pytest |
rstest |
|
| hypothesis | proptest |
Kiểm thử dựa trên thuộc tính (property-based testing). Xem Kiểm thử nâng cao |
unittest.mock |
mockall, hoặc tự viết bản giả (fake) triển khai trait của bạn |
|
| timeit, pytest-benchmark | criterion |
Xem Hiệu năng |
| cProfile, py-spy | cargo flamegraph, perf, hoặc bất kỳ profiler native nào |
|
| Docstring + Sphinx | Doc comment /// viết bằng Markdown + cargo doc |
Ví dụ code trong tài liệu được chạy như test. Xem Tài liệu |
| REPL của Python, Jupyter | Rust Playground (tiếng Anh); evcxr_repl và evcxr_jupyter |
Rust không có REPL có sẵn. Những dự án cargo run nhỏ là cách thử nghiệm thông thường |
| VS Code + Pylance, PyCharm | VS Code + rust-analyzer, hoặc JetBrains RustRover | rust-analyzer cho bạn gợi ý code, nhảy tới định nghĩa và báo lỗi ngay trong editor |
Đây là một Cargo.toml nhỏ, để bạn so sánh với một 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"] }Thư viện và framework: “Vậy thay vì … thì dùng gì?”
Phần tiêu đề “Thư viện và framework: “Vậy thay vì … thì dùng gì?””“Mình dùng requests, FastAPI và pandas. Trong Rust có gì?” Dưới đây là những thứ tương đương gần nhất. Hệ sinh thái Python lâu đời và lớn hơn nhiều, nên ở một số mảng (nhất là khoa học dữ liệu) Rust có ít lựa chọn hơn, và các lựa chọn đó cũng trẻ hơn.
Web và mạng
Phần tiêu đề “Web và mạng”| Python | Rust | Ghi chú |
|---|---|---|
| Flask, FastAPI | axum (chúng ta xây một cái trong Dự án: Web API), actix-web | Handler là các async fn bình thường; kiểu request và response lấy từ struct của bạn, hơi giống FastAPI với pydantic |
| Django | Không có thứ tương đương đầy đủ. Loco (loco-rs) là framework “có sẵn mọi thứ”; phần lớn các nhóm kết hợp axum + một ORM + một template engine |
Không có trang admin có sẵn |
| requests, httpx | reqwest (bất đồng bộ, có client blocking tùy chọn), ureq (đơn giản và blocking) |
Xem Lập trình mạng |
| aiohttp | reqwest + tokio |
|
| Jinja2 | minijinja (do chính tác giả của Jinja viết), tera, askama (template được kiểm tra lúc biên dịch) |
|
| BeautifulSoup | scraper |
Dữ liệu và cơ sở dữ liệu
Phần tiêu đề “Dữ liệu và cơ sở dữ liệu”| Python | Rust | Ghi chú |
|---|---|---|
| SQLAlchemy ORM | Diesel (truy vấn được kiểm tra kiểu lúc biên dịch), SeaORM (bất đồng bộ, kiểu entity) | Bài Cơ sở dữ liệu trình bày cả hai với code chạy được |
| psycopg, SQL thuần | sqlx: SQL thuần, được kiểm tra với cơ sở dữ liệu thật của bạn ngay lúc biên dịch | Bất đồng bộ, rất phổ biến, không phải ORM |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | apalis cho hàng đợi job; tokio::spawn cho tác vụ nền trong tiến trình |
Khoa học dữ liệu và học máy
Phần tiêu đề “Khoa học dữ liệu và học máy”| Python | Rust | Ghi chú |
|---|---|---|
| pandas | Polars | Được viết bằng Rust; API Python của nó, có thể bạn đã biết, được xây trên chính thư viện Rust đó |
| NumPy | ndarray |
Mảng N chiều có cắt lát và broadcasting; ít tiện ích đi kèm hơn NumPy nhiều |
| scikit-learn | linfa, smartcore |
Ít thuật toán hơn và cộng đồng nhỏ hơn nhiều |
| PyTorch | candle (crate candle-core), burn |
Tốt cho việc chạy mô hình; phần lớn việc huấn luyện vẫn diễn ra trong Python |
| matplotlib | plotters |
|
Module csv |
Crate csv + serde |
Những viên gạch dùng hằng ngày
Phần tiêu đề “Những viên gạch dùng hằng ngày”| Python | Rust | Ghi chú |
|---|---|---|
json |
serde + serde_json |
Tiêu chuẩn chung; còn làm được TOML, YAML và nhiều định dạng khác. Xem JSON với Serde |
| pydantic | serde để parse + validator hoặc garde cho quy tắc kiểm tra dữ liệu |
Struct của bạn chính là schema |
| argparse, click, typer | clap |
Khai báo đối số dưới dạng một struct. Xem Dự án: công cụ CLI với clap |
logging |
tracing, hoặc log + env_logger |
Xem Ghi log |
re |
regex |
Không có look-around hay backreference, nhưng đảm bảo thời gian tuyến tính; fancy-regex thì có. Xem Biểu thức chính quy |
datetime |
chrono hoặc jiff |
Xem Ngày và giờ |
random |
rand |
Xem Số ngẫu nhiên |
asyncio |
tokio |
Xem Async/Await |
multiprocessing.Pool.map |
rayon: đổi .iter() thành .par_iter() |
|
itertools |
Phần lớn đã có sẵn trong Iterator; crate itertools bổ sung phần còn lại |
|
os, pathlib, subprocess |
std::env, std::path, std::fs, std::process |
Xem Đọc ghi file, Môi trường và tiến trình |
Đặt cạnh nhau: class trở thành struct + impl
Phần tiêu đề “Đặt cạnh nhau: class trở thành struct + impl”Đây là một class Python nhỏ:
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} có {self.balance}"
account = Account("Ada", 100)account.deposit(50)print(account)Và cùng thứ đó bằng Rust:
use std::fmt;
struct Account { owner: String, balance: i64,}
impl Account { // `new` chỉ là một quy ước, không phải từ khóa. Không có tham số mặc định, // nên nơi gọi luôn truyền cả hai giá trị. fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// `&mut self`: phương thức này thay đổi tài khoản. fn deposit(&mut self, amount: i64) { self.balance += amount; }}
// Đây là __str__ của Rust.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} có {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}");}Kết quả:
Ada có 150Có vài điểm khác. Các field được khai báo từ đầu cùng với kiểu, nên bạn không thể thêm thuộc tính mới về sau như Python cho phép viết account.nickname = "A". Dữ liệu (struct) và hành vi (impl) nằm ở hai khối riêng biệt. Mỗi phương thức nói rõ nó chỉ đọc (&self) hay thay đổi (&mut self) giá trị, và vì deposit thay đổi tài khoản, biến phải được khai báo let mut. Rust không có tham số mặc định; nếu bạn muốn một tài khoản bắt đầu từ 0, bạn thêm một hàm thứ hai, chẳng hạn fn empty(owner: &str) -> Self.
Đặt cạnh nhau: duck typing trở thành trait
Phần tiêu đề “Đặt cạnh nhau: duck typing trở thành trait”Python không quan tâm object thuộc class nào, chỉ cần nó có đúng phương thức:
class Duck: def speak(self): return "Quack"
class Robot: def speak(self): return "Beep boop"
def introduce(thing): print(f"Nó nói: {thing.speak()}")
for thing in [Duck(), Robot()]: introduce(thing)
introduce(42) # AttributeError, nhưng chỉ khi dòng này chạyTrong Rust, điều kiện “có phương thức speak” được viết ra thành một trait, và mỗi kiểu tự đăng ký bằng một khối 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() }}
// Nhận bất kỳ kiểu nào triển khai Speak.fn introduce(thing: &dyn Speak) { println!("Nó nói: {}", 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); // lỗi biên dịch: `{integer}` không triển khai Speak}Kết quả:
Nó nói: QuackNó nói: Beep boopVề tinh thần thì đây vẫn là duck typing, nhưng được trình biên dịch kiểm tra: introduce(&42) là lỗi ngay trước khi chương trình chạy, chứ không phải một AttributeError trên production. Nếu bạn từng dùng typing.Protocol, trait sẽ rất tự nhiên với bạn. Một list chứa nhiều kiểu khác nhau cần Box<dyn Speak>, vì Rust phải biết cách gọi phương thức trên từng phần tử (xem Trait object). Và bạn có thể triển khai trait của riêng mình cho những kiểu không do bạn viết, như String hay i32.
Đặt cạnh nhau: None trở thành Option
Phần tiêu đề “Đặt cạnh nhau: None trở thành Option”Trong Python, giá trị nào cũng có thể âm thầm là None, và bạn chỉ phát hiện ra khi dùng nó:
def find_email(user_id): return emails.get(user_id) # trả về None nếu không có
print(find_email(1).upper())print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'Trong Rust, một giá trị có thể bị thiếu sẽ có kiểu Option<T>. Bạn không thể dùng nó như một giá trị bình thường cho tới khi đã xử lý trường hợp 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}: không có email trong hồ sơ"), } }
// Giống `find_email(3) or "không rõ"` của Python. let email = find_email(3).unwrap_or("không rõ".to_string()); println!("{email}");
// Thay đổi giá trị bên trong, nếu có. let upper = find_email(2).map(|e| e.to_uppercase()); println!("{upper:?}");}Kết quả:
3: không có email trong hồ sơkhông rõSome("[email protected]")Trình biên dịch Rust không cho bạn gọi .to_uppercase() trên một Option<String>, nên lỗi “'NoneType' object has no attribute” đơn giản là không thể xảy ra. Một String của Rust lúc nào cũng là một chuỗi thật. Hãy học các phương thức hỗ trợ ở trang Option, và cách viết ngắn if let Some(email) = ... ở trang If let / while let.
Đặt cạnh nhau: exception trở thành Result và ?
Phần tiêu đề “Đặt cạnh nhau: exception trở thành Result và ?”Trong Python, int() ném exception, và bạn bắt nó ở đâu đó phía trên call stack:
def parse_port(text): port = int(text) # có thể ném ValueError if not 1 <= port <= 65535: raise ValueError(f"cổng {port} nằm ngoài phạm vi cho phép") return port
for text in ["8080", "http", "70000"]: try: print(f"cổng {parse_port(text)}") except ValueError as e: print(f"lỗi: {e}")Trong Rust, một hàm có thể thất bại sẽ nói rõ điều đó trong kiểu trả về. Toán tử ? có nghĩa là “nếu bước này thất bại, trả lỗi về cho nơi gọi tôi ngay lập tức”. Nó làm đúng việc để exception nổi lên trên, nhưng bạn nhìn thấy nó ngay trong code:
fn parse_port(text: &str) -> Result<u32, String> { let port: u32 = text .parse() .map_err(|e| format!("{text:?} không phải là số ({e})"))?;
if !(1..=65535).contains(&port) { return Err(format!("cổng {port} nằm ngoài phạm vi cho phép")); } Ok(port)}
fn main() { for text in ["8080", "http", "70000"] { match parse_port(text) { Ok(port) => println!("cổng {port}"), Err(e) => println!("lỗi: {e}"), } }}Kết quả:
cổng 8080lỗi: "http" không phải là số (invalid digit found in string)lỗi: cổng 70000 nằm ngoài phạm vi cho phépPhần “invalid digit found in string” là thông báo lỗi có sẵn của Rust (nghĩa là “gặp chữ số không hợp lệ trong chuỗi”), nên nó vẫn là tiếng Anh.
Khác biệt lớn: trong Python, hàm nào cũng có thể ném exception, và chữ ký hàm không cho bạn biết điều đó. Trong Rust, Result là một phần của chữ ký hàm, nên nơi gọi không thể quên nó. Dùng String làm kiểu lỗi thì không sao khi đang học; dự án thật dùng enum lỗi của riêng mình (Lỗi tự định nghĩa) hoặc crate anyhow, cho cảm giác rất giống “ném exception gì cũng được” (anyhow và thiserror).
Đặt cạnh nhau: comprehension và dict trở thành iterator và HashMap
Phần tiêu đề “Đặt cạnh nhau: comprehension và dict trở thành iterator và HashMap”List comprehension và đếm bằng dict là chuyện hằng ngày trong 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}")Trong Rust, một comprehension trở thành một chuỗi iterator kết thúc bằng collect(), còn dict trở thành một 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; }
// Từ xuất hiện nhiều nhất đứng trước; bằng nhau thì xếp theo bảng chữ cái. 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}"); }}Kết quả:
["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]the: 3cat: 1end: 1Câu mẫu ở đây được giữ nguyên tiếng Anh, vì kết quả phụ thuộc vào độ dài tính bằng byte (len()), vào việc đổi chữ hoa và vào thứ tự bảng chữ cái; chữ tiếng Việt có dấu chiếm nhiều byte hơn và sẽ cho kết quả khác.
Chuỗi iterator đọc từ trái sang phải thay vì từ trong ra ngoài, nhưng ý tưởng thì như nhau. Giống một generator expression, nó là lười (lazy): chẳng có gì chạy cho tới khi collect(), sum(), count() hoặc một vòng lặp for kéo các giá trị đi qua. collect() cần biết phải tạo ra thứ gì, vì vậy loud có kiểu Vec<String>. Và hãy để ý bước sắp xếp: dict của Python nhớ thứ tự thêm vào, còn HashMap của Rust thì không, nên chúng ta luôn sắp xếp trước khi in. Việc sắp xếp được trình bày ở trang Sắp xếp và tìm kiếm.
Đặt cạnh nhau: GIL và luồng thật
Phần tiêu đề “Đặt cạnh nhau: GIL và luồng thật”Trong CPython tiêu chuẩn, Global Interpreter Lock (GIL) chỉ cho một luồng chạy bytecode Python tại một thời điểm. Luồng giúp ích khi bạn chờ mạng, nhưng những việc nặng về CPU như thế này không nhanh hơn khi thêm luồng, nên lập trình viên Python thường chuyển sang 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("tổng:", sum(counts))Rust không có GIL. Mỗi thread::spawn là một luồng thật của hệ điều hành, và chúng chạy trên các lõi CPU khác nhau cùng một lúc:
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)];
// Mỗi khoảng có luồng hệ điều hành riêng. Chúng thật sự chạy cùng lúc. 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} số nguyên tố"); } println!("tổng: {}", counts.iter().sum::<usize>());}Kết quả:
0..25000: 2762 số nguyên tố25000..50000: 2371 số nguyên tố50000..75000: 2260 số nguyên tố75000..100000: 2199 số nguyên tốtổng: 9592thread::scope chờ mọi luồng xong rồi mới trả về, nên các luồng có thể an toàn mượn dữ liệu cục bộ. Không có GIL, có thể bạn lo hai luồng cùng thay đổi một giá trị một lúc. Rust xử lý chuyện đó lúc biên dịch: dữ liệu dùng chung mà có thể thay đổi phải được bọc trong thứ gì đó như Arc<Mutex<T>>, nếu không chương trình sẽ không biên dịch (xem Trạng thái dùng chung). Trong dự án thật, crate rayon làm việc này còn ngắn gọn hơn, hơi giống multiprocessing.Pool.map nhưng dùng luồng:
use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();Python cũng đang thay đổi ở mảng này: Python 3.13 thêm một bản build “free-threaded” tùy chọn không có GIL. Nhưng CPython tiêu chuẩn mà hầu hết mọi người cài và triển khai vẫn có GIL, và nhiều C extension chưa sẵn sàng cho free threading.
Viết Python extension bằng Rust (PyO3 + maturin)
Phần tiêu đề “Viết Python extension bằng Rust (PyO3 + maturin)”Bạn không cần rời bỏ Python để dùng Rust. PyO3 (tiếng Anh) cho phép bạn viết một module Python bằng Rust, và maturin (tiếng Anh) build nó thành một wheel bình thường mà pip cài được. Polars, pydantic-core và nhiều thư viện Python nhanh khác được làm theo cách này.
Tạo một dự án (bên trong một môi trường ảo đã kích hoạt):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathmaturin viết sẵn Cargo.toml và pyproject.toml cho bạn. Sau đó đặt các hàm Rust của bạn vào src/lib.rs:
use pyo3::prelude::*;
/// Một module Python viết bằng Rust.#[pymodule]mod fastmath { use pyo3::prelude::*;
/// Cộng bình phương của một danh sách số. #[pyfunction] fn sum_of_squares(numbers: Vec<i64>) -> i64 { numbers.iter().map(|n| n * n).sum() }
/// Đếm số từ trong một đoạn văn bản. #[pyfunction] fn word_count(text: &str) -> usize { text.split_whitespace().count() }}Build nó vào môi trường ảo của bạn bằng maturin develop (thêm --release khi bạn cần tốc độ), rồi dùng nó từ Python như mọi module khác:
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 chuyển đổi kiểu cho bạn: một list số nguyên của Python trở thành Vec<i64>, một str trở thành &str, và doc comment trở thành docstring của Python. Truyền sai kiểu sẽ thành một TypeError bình thường của Python. Khi bạn sẵn sàng chia sẻ, maturin build --release tạo ra một wheel, và maturin publish đưa nó lên PyPI. Một quy tắc tốt: đo trước, rồi chỉ chuyển vòng lặp nóng nhất sang Rust. Việc đi qua lại giữa Python và Rust có một chi phí nhỏ, nên một lần gọi làm thật nhiều việc sẽ tốt hơn một triệu lần gọi nhỏ.
Những chỗ dễ vấp với lập trình viên Python
Phần tiêu đề “Những chỗ dễ vấp với lập trình viên Python”Ai đến từ Python cũng vấp phải cùng những ổ gà này. Biết trước sẽ giúp bạn nhiều.
Phép gán sẽ move giá trị
Phần tiêu đề “Phép gán sẽ move giá trị”Trong Python, b = a cho bạn hai cái tên cho cùng một list. Trong Rust, let b = a; chuyển (move) quyền sở hữu một Vec hay String sang b, và a không dùng được nữa:
fn main() { let a = vec![1, 2, 3]; let mut b = a; b.push(4); println!("{a:?}");}error[E0382]: borrow of moved value: `a` --> src/main.rs:5:16 |2 | let a = vec![1, 2, 3]; | - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait3 | let mut b = a; | - value moved here4 | b.push(4);5 | println!("{a:?}"); | ^ value borrowed here after move |help: consider cloning the value if the performance cost is acceptable |3 | let mut b = a.clone(); | ++++++++Trình biên dịch báo rằng a đã bị move sang b ở dòng 3, nên dòng 5 không được dùng nó nữa, và gợi ý gọi .clone(). Cách sửa là mượn (let b = &a;, nếu bạn chỉ cần đọc) hoặc chủ động tạo một bản sao thật (a.clone()). Quy tắc duy nhất này, quyền sở hữu, là ý tưởng mới quan trọng nhất với bạn. Hãy đọc trang đó và trang Mượn thật chậm.
Phần còn lại của danh sách
Phần tiêu đề “Phần còn lại của danh sách”- Số nguyên có kích thước.
intcủa Python không bao giờ tràn. Mộti32của Rust dừng ở 2.147.483.647; vượt quá sẽ gây panic trong bản build debug. Hãy chọni64hoặcu64cho số lớn, và xem Tràn số nguyên. - Phép chia và phép lấy dư hoạt động khác.
7 / 2là3trong Rust, vì cả hai vế đều là số nguyên (giống//của Python). Viết7.0 / 2.0để được3.5. Ngoài ra,-7 % 3là-1trong Rust nhưng là2trong Python; dùng(-7_i32).rem_euclid(3)để có kết quả như Python. - Không trộn lẫn các kiểu số.
let x: f64 = 2.5 * 3;không biên dịch, vì3là số nguyên. Hãy viết3.0, hoặc chuyển đổi bằngas f64(xem Ép kiểu). - Bạn không thể đánh chỉ số vào chuỗi.
word[0]là lỗi biên dịch, vì chuỗi Rust là UTF-8 và một “ký tự” có thể chiếm nhiều byte. Dùngword.chars().next()hoặcword.chars().nth(i). Xem Chuỗi. Stringvà&str. Python có một kiểustr. Rust có hai:Stringsở hữu văn bản của nó,&strthì mượn. Hàm thường nhận&strcòn struct thường lưuString.- Biến mặc định là bất biến. Viết
let mutkhi một giá trị cần thay đổi. Trình biên dịch sẽ nhắc khi bạn quên. - Dấu ngoặc nhọn và dấu chấm phẩy. Thụt lề không có ý nghĩa với Rust;
{ }mới có. Và dòng cuối của một hàm mà không có dấu chấm phẩy chính là giá trị trả về:fn double(x: i32) -> i32 { x * 2 }. - Mọi
ifvàmatchphải đầy đủ và đúng kiểu. Không có truthiness:if countkhông biên dịch, bạn viếtif count > 0. Mộtmatchphải xử lý mọi trường hợp. - HashMap không có thứ tự.
dictcủa Python nhớ thứ tự thêm vào;HashMapcủa Rust thì không. Hãy sắp xếp trước khi in, dùngBTreeMap, hoặc thêm crateindexmap. - Có một bước biên dịch. Không có REPL, và build sạch một dự án có nhiều crate mất một lúc. Dùng
cargo checktrong lúc sửa code (nó bỏ qua bước sinh mã và nhanh hơn nhiều), vàcargo run --releasekhi đo tốc độ. - Borrow checker sẽ phản đối. Những mẫu bình thường trong Python, như nhiều object cùng giữ và thay đổi một object dùng chung, hay object trỏ ngược về cha của nó, cần đến con trỏ thông minh hoặc một thiết kế khác. Hãy chuẩn bị cho vài tuần bực bội. Rồi mọi thứ sẽ dễ hơn nhiều.
Lộ trình học cho lập trình viên Python
Phần tiêu đề “Lộ trình học cho lập trình viên Python”Bạn không cần đọc trang này theo thứ tự. Là lập trình viên Python, lộ trình dưới đây dành thời gian cho chỗ Rust khác nhất so với những gì bạn biết:
- Bắt đầu với Rust và Công cụ và cài đặt: cài Rust và chạy
cargo new. - Biến, Kiểu dữ liệu và Tràn số nguyên: kiểu là thói quen mới đầu tiên.
- Hàm, If…else và Vòng lặp: đọc nhanh; để ý biểu thức và những dấu chấm phẩy bị bỏ đi.
- Quyền sở hữu, rồi Mượn, rồi Slice: những trang quan trọng nhất với bạn. Cứ từ từ.
- Chuỗi:
Stringvà&str, và tại sao bạn không thể đánh chỉ số vào chuỗi. - Struct và Enum, rồi match và So khớp mẫu.
- Option và Result, rồi Lỗi tự định nghĩa.
- Vector, HashMap và HashSet và BTreeMap.
- Closure và Iterator: comprehension và generator của bạn.
- Trait, Generic, Các trait thông dụng và Trait object.
- Module, Cargo và crate và Kiểm thử.
- Lifetime và Con trỏ thông minh.
- Lập trình đồng thời, Trạng thái dùng chung và Async/Await.
- JSON với Serde, rồi xây một công cụ CLI với clap hoặc một Web API.
Luyện tập
Phần tiêu đề “Luyện tập”Cả hai bài tập đều cho bạn một đoạn Python. Hãy chuyển nó sang Rust.
1. Chuyển một comprehension và một dict
Phần tiêu đề “1. Chuyển một comprehension và một dict”Chuyển đoạn code Python này. Gợi ý: để in tên theo thứ tự, hãy collect chúng vào một Vec<&str>, gọi .sort(), rồi .join(", "). Để tính trung bình, chuyển đổi bằng as f64 trước khi chia.
scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)print("đậu:", ", ".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"trung bình: {average:.1f}")Xem lời giải
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!("đậu: {}", 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!("trung bình: {average:.1}");}Kết quả:
đậu: Ada, Cy[1, 9, 25, 49, 81]trung bình: 63.0Duyệt qua một HashMap cho bạn các cặp tham chiếu, vì vậy bạn thấy **score và *name: chúng đi theo tham chiếu về tới giá trị. Ở đây sort() không phải tùy chọn: thiếu nó, các tên có thể ra theo bất kỳ thứ tự nào. Và cần total as f64 vì Rust không bao giờ lén biến một số nguyên thành số thực sau lưng bạn.
2. Chuyển try/except sang Result
Phần tiêu đề “2. Chuyển try/except sang Result”Chuyển đoạn code Python này. Mỗi dòng phải có dạng tên: tuổi. Gợi ý: line.split_once(':') trả về một Option<(&str, &str)>; biến nó thành Result bằng .ok_or(...), rồi dùng ?. Hàm của bạn nên trả về Result<(String, u32), String>.
def parse_line(line): if ":" not in line: raise ValueError("thiếu ':'") name, age = line.split(":", 1) try: return name.strip(), int(age.strip()) except ValueError as e: raise ValueError(f"tuổi không hợp lệ: {e}")
ages = {}for line in ["Ada: 36", "Grace 45", "Linus: bốn mươi"]: try: name, age = parse_line(line) ages[name] = age print(f"ok: {name} {age} tuổi") except ValueError as e: print(f"bỏ qua {line!r}: {e}")print("đã lưu", len(ages))Xem lời giải
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> { let (name, age) = line.split_once(':').ok_or("thiếu ':'")?; let age: u32 = age.trim().parse().map_err(|e| format!("tuổi không hợp lệ: {e}"))?; Ok((name.trim().to_string(), age))}
fn main() { let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: bốn mươi"] { match parse_line(line) { Ok((name, age)) => { println!("ok: {name} {age} tuổi"); ages.insert(name, age); } Err(e) => println!("bỏ qua {line:?}: {e}"), } }
println!("đã lưu {}", ages.len());}Kết quả:
ok: Ada 36 tuổibỏ qua "Grace 45": thiếu ':'bỏ qua "Linus: bốn mươi": tuổi không hợp lệ: invalid digit found in stringđã lưu 1Cả hai câu lệnh raise trở thành giá trị Err, còn try/except trở thành một match. Dấu ? sau ok_or("thiếu ':'") vẫn hoạt động dù lỗi là một &str còn hàm trả về lỗi kiểu String, vì ? chuyển đổi lỗi bằng From. Hãy để ý thứ tự bên trong nhánh Ok: chúng ta in trước, rồi insert mới move name vào map. Đảo hai dòng đó và trình biên dịch sẽ báo rằng name đã bị move.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Phần lớn Python mang sang được: vòng lặp trên iterator, tuple, closure,
match, và cách xử lý dữ liệu kiểu comprehension (dưới dạng chuỗi iterator). - Những thay đổi lớn là kiểu tĩnh được kiểm tra trước khi chạy, quyền sở hữu thay cho bộ thu gom rác,
Optionthay choNone, vàResult+?thay cho exception. - Class trở thành
struct+impl, duck typing trở thành trait, và khốiwithtrở thànhDroptự động.HashMapkhông có thứ tự, và số nguyên có kích thước cố định. - Rust không có GIL: các luồng chạy song song, và trình biên dịch ngăn data race.
- Cargo thay thế pip, venv và những người bạn;
cargo fmt,cargo clippyvàcargo testthay thế black, ruff và pytest. - Bạn có thể kết hợp cả hai ngôn ngữ: PyO3 và maturin biến code Rust thành một module Python.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Rust dùng gì thay cho None của Python?
2. Trong Python, b = a cho hai cái tên cùng chỉ một list. Còn let b = a; với một Vec trong Rust thì làm gì?
3. Trong Rust, 7 / 2 cho kết quả gì khi cả hai số đều là số nguyên?
4. Bạn muốn tăng tốc một hàm chậm trong dự án Python bằng Rust. Bạn dùng công cụ nào?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- The Rust Programming Language (tiếng Anh): cuốn sách chính thức. Chương 4 (ownership), 6 (enum và
Option), 9 (lỗi) và 13 (closure và iterator) quan trọng nhất với lập trình viên Python. - PyO3 user guide (tiếng Anh): mọi thứ về việc gọi Rust từ Python (và gọi Python từ Rust).
- Maturin user guide (tiếng Anh): build, kiểm thử và publish các gói Python chạy bằng Rust.
- Rust by Example (tiếng Anh): những ví dụ ngắn chạy được, phù hợp nếu bạn học tốt nhất bằng cách đọc code.
- Are We Learning Yet? (tiếng Anh): tổng quan về các crate học máy và dữ liệu của Rust.