Bỏ qua để đến nội dung

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 import nó 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.

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

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

Đâ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.

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
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
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
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ó 150
▶ Tự chạy thử

Có 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ạy

Trong 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: Quack
Nó nói: Beep boop
▶ Tự chạy thử

Về 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.

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):
emails = {1: "[email protected]", 2: "[email protected]"}
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 {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => None,
}
}
fn main() {
for id in [1, 3] {
match find_email(id) {
Some(email) => println!("{id}: {}", email.to_uppercase()),
None => println!("{id}: 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õ
▶ Tự chạy thử

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 8080
lỗ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ép
▶ Tự chạy thử

Phầ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: 3
cat: 1
end: 1
▶ Tự chạy thử

Câ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.

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: 9592
▶ Tự chạy thử

thread::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):

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

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

PyO3 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.

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` trait
3 | let mut b = a;
| - value moved here
4 | b.push(4);
5 | println!("{a:?}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut b = a.clone();
| ++++++++

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.

  • Số nguyên có kích thước. int của Python không bao giờ tràn. Một i32 của Rust dừng ở 2.147.483.647; vượt quá sẽ gây panic trong bản build debug. Hãy chọn i64 hoặc u64 cho 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 / 2 là 3 trong Rust, vì cả hai vế đều là số nguyên (giống // của Python). Viết 7.0 / 2.0 để được 3.5. Ngoài ra, -7 % 3 là -1 trong Rust nhưng là 2 trong 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ì 3 là số nguyên. Hãy viết 3.0, hoặc chuyển đổi bằng as 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ùng word.chars().next() hoặc word.chars().nth(i). Xem Chuỗi.
  • String và &str. Python có một kiểu str. Rust có hai: String sở hữu văn bản của nó, &str thì mượn. Hàm thường nhận &str còn struct thường lưu String.
  • Biến mặc định là bất biến. Viết let mut khi 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 if và match phải đầy đủ và đúng kiểu. Không có truthiness: if count không biên dịch, bạn viết if count > 0. Một match phải xử lý mọi trường hợp.
  • HashMap không có thứ tự. dict của Python nhớ thứ tự thêm vào; HashMap của Rust thì không. Hãy sắp xếp trước khi in, dùng BTreeMap, hoặc thêm crate indexmap.
  • 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 check trong lúc sửa code (nó bỏ qua bước sinh mã và nhanh hơn nhiều), và cargo run --release khi đ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.

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:

  1. Bắt đầu với Rust và Công cụ và cài đặt: cài Rust và chạy cargo new.
  2. Biến, Kiểu dữ liệu và Tràn số nguyên: kiểu là thói quen mới đầu tiên.
  3. 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.
  4. 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ừ.
  5. Chuỗi: String và &str, và tại sao bạn không thể đánh chỉ số vào chuỗi.
  6. Struct và Enum, rồi match và So khớp mẫu.
  7. Option và Result, rồi Lỗi tự định nghĩa.
  8. Vector, HashMap và HashSet và BTreeMap.
  9. Closure và Iterator: comprehension và generator của bạn.
  10. Trait, Generic, Các trait thông dụng và Trait object.
  11. Module, Cargo và crate và Kiểm thử.
  12. Lifetime và Con trỏ thông minh.
  13. Lập trình đồng thời, Trạng thái dùng chung và Async/Await.
  14. JSON với Serde, rồi xây một công cụ CLI với clap hoặc một Web API.

Cả hai bài tập đều cho bạn một đoạn Python. Hãy chuyển nó sang Rust.

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.0
▶ Tự chạy thử

Duyệ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.

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ổi
bỏ 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 1
▶ Tự chạy thử

Cả 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.

  • 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, Option thay cho None, và Result + ? thay cho exception.
  • Class trở thành struct + impl, duck typing trở thành trait, và khối with trở thành Drop tự động. HashMap khô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 clippy và cargo test thay 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

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

  • 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.