- Phân biệt rạch ròi 4 cấu trúc built-in: khi nào
list, khi nàotuple, khi nàodict, khi nàoset. - Hiểu khái niệm hashable — lý do tuple làm được dict key còn list thì không.
- Master slicing
[start:stop:step]— áp dụng cho list/tuple/string/bytes. - Master comprehension (list / dict / set / generator) — viết Pythonic, không loop tay.
- Biết khi nào dùng generator expression thay list (lazy, tiết kiệm RAM).
- Biết các helper stdlib:
Counter,defaultdict,deque,namedtuple. - Hiểu dict ordered từ Python 3.7+ — không cần
OrderedDicttrừ trường hợp đặc biệt.
1. list — chuỗi có thứ tự, có thể thay đổi
list là cấu trúc dữ liệu được dùng nhiều nhất Python. Nó là mutable sequence:
giữ thứ tự chèn, cho phép trùng, cho phép sửa/thêm/xoá in-place. Implementation bên dưới là
dynamic array (resize khi đầy) — không phải linked list.
# 1. Literal — cách dùng 99% trường hợp
nums = [1, 2, 3, 4]
mixed = [1, "hai", 3.0, True, None] # Python list cho phép mọi type, trộn lẫn được
# 2. Constructor list() từ iterable bất kỳ
chars = list("abc") # ['a', 'b', 'c']
rng = list(range(5)) # [0, 1, 2, 3, 4]
# 3. Repeat operator * — cẩn thận với mutable reference!
zeros = [0] * 5 # [0, 0, 0, 0, 0] — OK với int
grid = [[0] * 3] * 3 # 🔥 3 reference TRỎ CÙNG 1 inner list!
grid[0][0] = 9
print(grid) # [[9,0,0],[9,0,0],[9,0,0]] ← sai!
# Cách đúng tạo 2D list:
grid = [[0] * 3 for _ in range(3)] # 3 inner list độc lập
# Truy cập (zero-indexed). Index âm đếm từ cuối
nums[0] # 1 — phần tử đầu
nums[-1] # 4 — phần tử cuối
len(nums) # 4
[[0]*3]*3 không tạo ma trận 3x3 đúng — Python tạo một inner list rồi nhân 3
reference. Sửa 1 phần tử inner sẽ "lan" sang 2 hàng còn lại. Quy tắc: mỗi khi nhân list
chứa mutable, hãy dùng comprehension thay vì *.
1.1. Method của list
| Method | Mô tả | Mutate? | Trả về |
|---|---|---|---|
append(x) | Thêm 1 phần tử vào cuối | Có | None |
extend(it) | Nối từng phần tử của iterable vào cuối | Có | None |
insert(i, x) | Chèn tại index i (O(n) vì shift) | Có | None |
pop(i=-1) | Xoá & trả phần tử (mặc định cuối) | Có | phần tử bị xoá |
remove(x) | Xoá phần tử đầu tiên bằng x; lỗi nếu không có | Có | None |
clear() | Xoá hết phần tử | Có | None |
sort(key=..., reverse=...) | Sort in-place | Có | None |
reverse() | Đảo ngược in-place | Có | None |
index(x) | Trả index đầu tiên của x; lỗi nếu không có | Không | int |
count(x) | Đếm số lần x xuất hiện | Không | int |
copy() | Shallow copy (giống lst[:]) | Không | list |
lst = [3, 1, 4]
lst.append(5) # [3, 1, 4, 5]
lst.extend([9, 2]) # [3, 1, 4, 5, 9, 2]
lst.insert(0, 0) # [0, 3, 1, 4, 5, 9, 2] (chèn đầu)
last = lst.pop() # last = 2; lst = [0, 3, 1, 4, 5, 9]
lst.remove(1) # xoá giá trị 1 đầu tiên
# sort() in-place vs sorted() trả mới
lst.sort() # mutate lst, return None
new = sorted(lst, reverse=True) # trả list mới, lst nguyên vẹn
# Sort theo key (giống ORDER BY trong SQL)
people = [("An", 25), ("Bình", 17), ("Cường", 30)]
people.sort(key=lambda p: p[1]) # sort theo tuổi
# index / count — KHÔNG mutate
lst = [1, 2, 3, 2, 1]
lst.index(2) # 1 — lần xuất hiện đầu
lst.count(1) # 2
sort() trả None
Lỗi cực phổ biến với người mới: x = lst.sort() — x sẽ là None!
list.sort() mutate và trả None (convention Python: method mutate không trả
value để tránh nhầm). Cần list mới sort, dùng sorted(lst).
2. tuple — chuỗi có thứ tự, bất biến
tuple giống list ở chỗ có thứ tự, cho phép trùng, truy cập theo index. Khác
ở chỗ immutable: tạo xong không sửa được. Đổi lại, tuple hashable (nếu các
phần tử trong nó cũng hashable) — dùng được làm dict key hoặc set element.
# Có thể có hoặc không có dấu ngoặc
t1 = (1, 2, 3)
t2 = 1, 2, 3 # giống y hệt t1 — dấu phẩy mới tạo tuple, không phải ()
# 🔥 Gotcha: 1 phần tử BẮT BUỘC có dấu phẩy
a = (1) # 🛑 đây là int 1, không phải tuple
b = (1,) # ✅ tuple 1 phần tử
type(a), type(b) # <class 'int'>, <class 'tuple'>
# Tuple rỗng
empty = () # hoặc tuple()
# Từ iterable
tuple([1, 2, 3]) # (1, 2, 3)
tuple("abc") # ('a', 'b', 'c')
# Method ít — chỉ có count và index (không mutate được)
t = (1, 2, 3, 2, 1)
t.count(2) # 2
t.index(3) # 2
# Thử sửa → TypeError
t[0] = 99 # TypeError: 'tuple' object does not support item assignment
list hay tuple?- list khi bạn sẽ sửa/thêm/xoá dần (sequence động): danh sách user, todo, kết quả query.
- tuple khi gói nhiều giá trị thành record cố định: toạ độ
(x, y), RGB(r, g, b), return nhiều giá trị. - Quy tắc tinh thần: list = "danh sách đồng dạng" (nhiều thứ cùng kiểu), tuple = "bộ ghi danh" (vài trường khác nhau).
- Tuple hashable → dùng được làm dict key, set element. List không hashable.
- Tuple tiết kiệm memory hơn chút ít vì không cần slot dư cho việc grow.
3. Tuple unpacking — gán nhiều biến cùng lúc
Đây là tính năng đẹp nhất của Python cho việc xử lý tuple. Nhờ nó, bạn ít khi phải
gọi t[0], t[1]... mà tách thẳng thành biến có tên.
# 1. Cơ bản
x, y = 1, 2 # x=1, y=2
a, b, c = [10, 20, 30] # a=10, b=20, c=30 — unpack được mọi iterable
# 2. Swap không cần biến tạm — idiom Pythonic kinh điển
a, b = b, a
# 3. Return nhiều giá trị từ function (thực ra là return 1 tuple)
def divmod_simple(a, b):
return a // b, a % b # tuple ngầm
q, r = divmod_simple(17, 5) # q=3, r=2
# 4. Star unpacking — gom phần còn lại
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4] # init=[1,2,3], last=4
head, *mid, tail = [1, 2, 3, 4, 5] # head=1, mid=[2,3,4], tail=5
# 5. Bỏ qua giá trị bằng _ (convention)
_, year, _ = ("2026-05-20").split("-") # lấy year, bỏ 2 phần
# 6. Nested unpacking
((a, b), c) = ((1, 2), 3) # a=1, b=2, c=3
# 7. Unpacking trong vòng lặp — siêu phổ biến với dict.items()
pairs = [("a", 1), ("b", 2), ("c", 3)]
for key, value in pairs:
print(f"{key}: {value}")
So sánh x, y, z = point vs x = point[0]; y = point[1]; z = point[2]. Cái đầu
ngắn hơn, đọc rõ tên, và Python đảm bảo length đúng (nếu lệch sẽ throw ValueError —
giúp catch bug sớm). Cái sau im lặng nếu length không đúng.
4. dict — bảng băm key → value
dict là cấu trúc dữ liệu quan trọng số một Python — dùng nó nhiều hơn list. Nó là
hash map: tra cứu / chèn / xoá theo key đều O(1) trung bình. Mọi key phải
hashable (immutable + có __hash__).
# 1. Literal
user = {"name": "An", "age": 25, "city": "Hà Nội"}
# 2. Constructor dict() — keyword form (key phải là valid identifier)
user2 = dict(name="Bình", age=22)
# 3. Từ iterable of pairs
user3 = dict([("name", "Cường"), ("age", 30)])
# 4. dict comprehension (xem mục 9)
sq = {x: x**2 for x in range(5)} # {0:0, 1:1, 2:4, 3:9, 4:16}
# --- Truy cập ---
user["name"] # 'An'
user["unknown"] # KeyError: 'unknown' 🔥
user.get("name") # 'An' — an toàn, không lỗi
user.get("unknown") # None
user.get("unknown", 0) # 0 — default tự chọn
# Check key tồn tại
"name" in user # True — O(1) hash lookup, nhanh!
"age" not in user # False
d[k] vs d.get(k)
Quy tắc: d[k] dùng khi bạn chắc chắn key tồn tại (hoặc cố tình muốn raise nếu
không). d.get(k, default) dùng khi key có thể không có và bạn có giá trị fallback.
Đây là biểu hiện của triết lý EAFP (Easier to Ask Forgiveness than Permission) vs
LBYL (Look Before You Leap). Python idiomatic thường dùng EAFP với try/except, nhưng
với dict thì cả hai đều OK — .get() ngắn hơn.
4.1. Method của dict
| Method | Mô tả |
|---|---|
d[k] = v | Thêm hoặc cập nhật |
d.get(k, default=None) | Lấy giá trị, có default an toàn |
d.setdefault(k, v) | Nếu k chưa có thì d[k]=v; luôn trả giá trị hiện tại |
d.pop(k, default) | Xoá & trả value (default nếu không có) |
d.popitem() | Xoá & trả cặp cuối được chèn (LIFO từ 3.7+) |
d.update(other) | Merge từ dict/iterable khác (other override) |
d.keys() | View các key (live view, không phải snapshot) |
d.values() | View các value |
d.items() | View các (key, value) tuple |
d.clear() | Xoá tất cả |
d.copy() | Shallow copy |
d1 | d2 | Merge (Python 3.9+), trả dict mới |
d1 |= d2 | Update in-place (3.9+) |
# setdefault — pattern xây dict-of-list không cần check key
words = ["apple", "ant", "bat", "banana", "cat"]
groups = {}
for w in words:
groups.setdefault(w[0], []).append(w)
# {'a': ['apple','ant'], 'b': ['bat','banana'], 'c': ['cat']}
# defaultdict (xem mục 11) thanh lịch hơn cho pattern này.
# items() — iterate cả key + value
for k, v in user.items():
print(f"{k} = {v}")
# update — merge
defaults = {"theme": "light", "lang": "vi"}
prefs = {"theme": "dark"}
defaults.update(prefs) # defaults = {'theme': 'dark', 'lang': 'vi'}
# Python 3.9+ — merge operator | (giống Object spread của JS)
merged = defaults | prefs # mới, không mutate
5. Dict ordered từ Python 3.7+
Dict bảo toàn thứ tự chèn (insertion-ordered)
Trước Python 3.6, dict không đảm bảo thứ tự — iterate ra theo hash bucket,
có thể khác nhau giữa các run. Người ta phải dùng collections.OrderedDict nếu cần thứ tự.
Từ CPython 3.6 (implementation detail) và Python 3.7+ (chính thức
vào language spec), dict bảo toàn insertion order: iterate ra theo
đúng thứ tự bạn chèn. Vì vậy bây giờ bạn ít khi cần OrderedDict — chỉ dùng khi cần
method đặc biệt như move_to_end().
d = {}
d["c"] = 3
d["a"] = 1
d["b"] = 2
list(d) # ['c', 'a', 'b'] — đúng thứ tự chèn, không sort
list(d.items()) # [('c', 3), ('a', 1), ('b', 2)]
# Update giá trị key đã có KHÔNG đổi vị trí
d["c"] = 99
list(d) # ['c', 'a', 'b'] — 'c' vẫn ở đầu
# Xoá + chèn lại = đẩy 'c' về cuối
del d["c"]
d["c"] = 99
list(d) # ['a', 'b', 'c']
6. set — tập hợp unique, không thứ tự
set là tập hợp các phần tử duy nhất, hashable, không có thứ tự. Dùng để
loại trùng (dedupe), check membership cực nhanh (O(1) hash lookup), và thực hiện phép toán tập hợp
(union, intersection, difference).
# 1. Literal
s = {1, 2, 3}
# 🔥 Gotcha: {} là DICT rỗng, không phải set rỗng
empty_dict = {} # dict, không phải set
empty_set = set() # cách duy nhất tạo set rỗng
type({}) # <class 'dict'>
type(set()) # <class 'set'>
# 2. Từ iterable — dedupe trick kinh điển
set([1, 2, 2, 3, 3, 3]) # {1, 2, 3}
set("mississippi") # {'m', 'i', 's', 'p'}
# --- Thao tác cơ bản ---
s.add(4) # {1, 2, 3, 4}
s.add(2) # không thay đổi — đã có 2
s.discard(5) # OK, không lỗi nếu không có
s.remove(5) # KeyError nếu không có
s.pop() # xoá & trả 1 phần tử bất kỳ
# Membership — siêu nhanh O(1)
2 in s # True
6.1. Phép toán tập hợp
| Phép toán | Operator | Method | Ý nghĩa |
|---|---|---|---|
| Hợp (union) | a | b | a.union(b) | Mọi phần tử của a hoặc b |
| Giao (intersection) | a & b | a.intersection(b) | Phần tử có trong cả a và b |
| Hiệu (difference) | a - b | a.difference(b) | Trong a nhưng không trong b |
| Hiệu đối xứng | a ^ b | a.symmetric_difference(b) | Trong a hoặc b nhưng không cả hai |
| Tập con | a <= b | a.issubset(b) | Mọi phần tử của a có trong b |
| Tập cha | a >= b | a.issuperset(b) | Mọi phần tử của b có trong a |
| Rời nhau | — | a.isdisjoint(b) | Không có phần tử chung |
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b # {1, 2, 3, 4, 5, 6} hợp
a & b # {3, 4} giao
a - b # {1, 2} a trừ b
a ^ b # {1, 2, 5, 6} hiệu đối xứng
{1, 2} <= a # True — {1,2} là tập con của a
a.isdisjoint({7, 8}) # True — không có phần tử chung
# Use case thực tế: tìm common tags
post1_tags = {"python", "web", "flask"}
post2_tags = {"python", "data", "web"}
common = post1_tags & post2_tags # {'python', 'web'}
7. frozenset — set bất biến
frozenset giống set nhưng immutable: tạo xong không add/remove
được. Vì immutable nên nó hashable → dùng được làm dict key hoặc làm phần tử của một set khác.
fs = frozenset([1, 2, 3])
fs.add(4) # AttributeError — không có method add
# Vẫn dùng được mọi phép | & - ^
fs | {4} # frozenset({1, 2, 3, 4})
# Use case: set chứa các "tổ hợp" không thay đổi
combos = {
frozenset(["red", "green"]),
frozenset(["blue", "yellow"]),
} # OK — frozenset hashable
# dict key là một tổ hợp role chẳng hạn
permissions = {
frozenset(["read"]): "viewer",
frozenset(["read", "write"]): "editor",
frozenset(["read", "write", "admin"]): "owner",
}
Một object hashable nếu nó có giá trị hash không đổi suốt vòng đời (__hash__)
và so sánh được với object khác (__eq__). Quy tắc dễ nhớ:
- Hashable:
int,float,bool,str,tuple(nếu mọi phần tử cũng hashable),frozenset,None. - Không hashable:
list,dict,set, hầu hết class tự định nghĩa (trừ khi có__hash__).
Quy tắc trực giác: mutable thì không hashable. Vì nếu hash thay đổi sau khi đã lưu vào set/dict, ta không thể tìm lại được nó.
8. Slicing — [start:stop:step]
Một trong những tính năng đẹp nhất Python. Áp dụng cho mọi sequence: list,
tuple, str, bytes, range.
s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Cú pháp: s[start:stop:step]
# - start mặc định 0 (hoặc len-1 nếu step âm)
# - stop mặc định len (hoặc -len-1 nếu step âm). KHÔNG bao gồm stop!
# - step mặc định 1
s[2:5] # [2, 3, 4] — từ index 2 đến 4 (5 không gồm)
s[:3] # [0, 1, 2] — 3 phần tử đầu
s[-3:] # [7, 8, 9] — 3 phần tử cuối
s[:] # bản copy của toàn bộ s (shallow)
s[::2] # [0, 2, 4, 6, 8] — mọi index chẵn
s[1::2] # [1, 3, 5, 7, 9] — mọi index lẻ
s[::-1] # [9, 8, 7, ... 0] ← REVERSE idiom kinh điển
s[8:2:-1] # [8, 7, 6, 5, 4, 3] — từ 8 ngược về 3
# Out-of-range KHÔNG lỗi (khác với indexing!)
s[100:200] # [] — slice rỗng, không IndexError
s[100] # IndexError 🔥
# String là sequence của ký tự → slicing hoạt động như list
"Python"[::-1] # 'nohtyP'
"Hello World"[6:] # 'World'
# Slice assignment — chỉ với list (mutable sequence)
lst = [1, 2, 3, 4, 5]
lst[1:3] = [20, 30] # [1, 20, 30, 4, 5]
lst[1:3] = [] # [1, 4, 5] — xoá range bằng gán list rỗng
- Stop không bao gồm — half-open interval
[start, stop). Vì vậys[0:len(s)]= toàn bộ s, và length của slice =stop - start(khi step=1). - Slicing không bao giờ raise IndexError — out-of-range tự cắt về cuối. Khác với
s[i]. - Step âm = duyệt ngược.
s[::-1]là idiom reverse phổ biến nhất. - Shallow copy:
s[:]hoặcs.copy()hoặclist(s)— cả 3 tương đương.
9. Comprehension — viết Pythonic, không loop tay
Comprehension là chữ ký Python. Một dòng comprehension thay được 3-5 dòng for-loop +
append. Có 4 loại: list / dict / set / generator.
Cấu trúc comprehension
Đọc từ trái sang phải: output ← nguồn ← điều kiện.
[x ** 2 for x in range(10) if x % 2 == 0] → [0, 4, 16, 36, 64]
9.1. List comprehension
# So sánh: loop truyền thống vs comprehension
# 1. Loop truyền thống — 4 dòng
squares = []
for x in range(10):
squares.append(x ** 2)
# 2. List comprehension — 1 dòng
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 3. Có filter (if)
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]
# 4. Có if/else trong expression (chú ý vị trí trước for)
labels = ["chẵn" if x % 2 == 0 else "lẻ" for x in range(5)]
# ['chẵn', 'lẻ', 'chẵn', 'lẻ', 'chẵn']
# 5. Nested loop — đọc giống for/for lồng nhau
pairs = [(x, y) for x in [1, 2, 3] for y in ['a', 'b']]
# [(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]
# 6. Flatten 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 7. Transpose ma trận
T = [[row[i] for row in matrix] for i in range(3)]
# [[1,4,7], [2,5,8], [3,6,9]]
9.2. Dict / Set comprehension
# Dict comprehension — key: value
sq_dict = {x: x ** 2 for x in range(5)}
# {0:0, 1:1, 2:4, 3:9, 4:16}
# Invert dict (swap key/value)
d = {"a": 1, "b": 2, "c": 3}
inv = {v: k for k, v in d.items()}
# {1: 'a', 2: 'b', 3: 'c'}
# Filter dict theo value
prices = {"a": 100, "b": 250, "c": 50}
expensive = {k: v for k, v in prices.items() if v > 100}
# {'b': 250}
# Set comprehension
mod5 = {x % 5 for x in range(20)}
# {0, 1, 2, 3, 4} — tự dedupe
# Unique vowels từ string
vowels = {c for c in "hello world" if c in "aeiou"}
# {'e', 'o'}
9.3. Generator expression — lazy
Generator expression có cú pháp giống hệt list comprehension nhưng dùng dấu ngoặc tròn
(). Khác biệt then chốt: lazy — không tạo list trong RAM, sinh
từng phần tử khi được yêu cầu.
# List comprehension — EAGER, tạo list ngay
lst = [x ** 2 for x in range(10_000_000)]
# Cấp phát ~80MB RAM ngay lập tức 🔥
# Generator expression — LAZY, chỉ tính khi cần
gen = (x ** 2 for x in range(10_000_000))
# Cấp phát ~200 byte — chỉ là object generator!
# Tính tổng — không cần lưu trung gian
total = sum(x ** 2 for x in range(10_000_000))
# Idiom: khi truyền vào sum/min/max/any/all/... có thể BỎ () thừa
# Iterate qua từng phần tử
for v in gen:
if v > 1000:
break
# 🔥 Generator dùng 1 lần — sau khi cạn không reset được
gen2 = (x * 2 for x in range(3))
list(gen2) # [0, 2, 4]
list(gen2) # [] — đã cạn!
- List khi cần dùng nhiều lần, cần
len(), cần index, cần sort, cần truyền cho hàm yêu cầu sequence. - Generator khi chỉ duyệt 1 lần (đặc biệt là khi truyền vào
sum/any/all/min/max), hoặc khi dữ liệu rất lớn / vô hạn. - Chương 6 sẽ đào sâu generator (
yield, lazy,itertools).
10. collections.Counter — đếm tần suất
Counter là dict subclass chuyên đếm. Khởi tạo từ iterable thì nó tự đếm số lần
xuất hiện. Dùng cho thống kê tần suất ký tự, từ, log entry, v.v.
from collections import Counter
# 1. Từ string — đếm ký tự
c = Counter("mississippi")
# Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
c["s"] # 4
c["z"] # 0 — Counter trả 0 cho key vắng (KHÔNG raise KeyError)
# 2. Từ list of words
words = "to be or not to be".split()
wc = Counter(words)
# Counter({'to': 2, 'be': 2, 'or': 1, 'not': 1})
# 3. most_common(n) — top n phổ biến nhất
wc.most_common(2)
# [('to', 2), ('be', 2)]
# 4. Update / subtract
c.update("missing") # cộng dồn count
c.subtract("mi") # trừ count
# 5. Math operations — cộng / trừ 2 Counter
a = Counter("abracadabra")
b = Counter("alacazam")
a + b # cộng count tương ứng
a - b # trừ (không cho âm)
a & b # min(a, b) — intersection
a | b # max(a, b) — union
# 6. Total + reset
c.total() # 3.10+ — tổng count
+c # bỏ count ≤ 0
11. collections.defaultdict — tự khởi tạo default
defaultdict(factory) là dict subclass: khi truy cập key chưa có, nó tự
tạo bằng cách gọi factory(). Tránh được boilerplate if k not in d:.
from collections import defaultdict
# 1. dict-of-list — gom nhóm
words = ["apple", "ant", "bat", "banana", "cat"]
groups = defaultdict(list)
for w in words:
groups[w[0]].append(w)
# defaultdict(list, {'a': ['apple','ant'], 'b': ['bat','banana'], 'c': ['cat']})
# So với dict thường — phải setdefault hoặc check:
# if w[0] not in groups: groups[w[0]] = []
# groups[w[0]].append(w)
# 2. dict-of-int — đếm (tương đương Counter)
counts = defaultdict(int)
for c in "mississippi":
counts[c] += 1
# {'m': 1, 'i': 4, 's': 4, 'p': 2}
# 3. dict-of-set — gom unique
tags_by_user = defaultdict(set)
events = [("an", "login"), ("an", "login"), ("binh", "click")]
for user, tag in events:
tags_by_user[user].add(tag)
# {'an': {'login'}, 'binh': {'click'}}
# 4. Nested defaultdict
tree = defaultdict(lambda: defaultdict(int))
tree["a"]["x"] = 1
tree["a"]["y"] = 2
tree["b"]["z"] = 3
12. collections.deque — hàng đợi hai đầu
deque (double-ended queue) cho phép thêm/xoá ở cả 2 đầu trong O(1). Với
list, list.insert(0, x) là O(n) — phải shift toàn bộ. Vì vậy khi cần queue
hoặc sliding window, dùng deque.
from collections import deque
# Cơ bản
dq = deque([1, 2, 3])
dq.append(4) # deque([1, 2, 3, 4])
dq.appendleft(0) # deque([0, 1, 2, 3, 4]) — O(1)!
dq.pop() # 4 — từ phải
dq.popleft() # 0 — từ trái, O(1)
# Queue FIFO
queue = deque()
queue.append("job1")
queue.append("job2")
queue.popleft() # 'job1' — first in first out
# Bounded deque — như buffer cố định kích thước
recent = deque(maxlen=3)
for i in range(5):
recent.append(i)
list(recent) # [2, 3, 4] — chỉ giữ 3 cuối
# Rotate — xoay vòng
dq = deque([1, 2, 3, 4, 5])
dq.rotate(2) # deque([4, 5, 1, 2, 3])
dq.rotate(-1) # deque([5, 1, 2, 3, 4])
list vs deque- append cuối: list O(1) amortized, deque O(1). Cả hai OK.
- append/pop đầu: list O(n) (phải shift), deque O(1). Deque thắng tuyệt đối.
- index ngẫu nhiên: list O(1), deque O(n). List thắng — deque không có random access nhanh.
- Slicing: chỉ list hỗ trợ. Deque phải convert ra list trước.
- Quy tắc: cần queue / sliding window / append đầu →
deque. Còn lại dùnglist.
13. namedtuple & dataclass — tuple có tên
Khi viết point[0], point[1] nhiều, code mất ý nghĩa. namedtuple
tạo subclass của tuple có tên field — vừa giữ ưu điểm immutable/hashable của tuple,
vừa truy cập bằng .x, .y rõ ràng.
from collections import namedtuple
# 1. Tạo namedtuple class
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x, p.y # 3, 4 — truy cập bằng tên
p[0], p[1] # 3, 4 — vẫn truy cập bằng index được
x, y = p # unpacking như tuple thường
isinstance(p, tuple) # True — là subclass của tuple
# Immutable
p.x = 99 # AttributeError
# 2. Class-based syntax (typing.NamedTuple) — có type hints
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
city: str = "Hà Nội" # default value
u = User("An", 25)
u.name # 'An'
# 3. dataclass (chương 5 sẽ đào sâu) — mutable + nhiều tính năng hơn
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: int = 0
prod = Product("Book", 19.99)
prod.price = 14.99 # OK — dataclass mặc định mutable
- tuple: gói nhanh 2-3 giá trị, không có ý nghĩa lâu dài.
- namedtuple: record bất biến, cần hashable (làm dict key).
- dataclass: record có thể thay đổi, nhiều method, validation, default phức tạp. Mặc định hiện đại (chương 5).
- dict: schema động, key thay đổi theo runtime (config JSON).
Bài tập
Bài 1 — count_vowels(s: str) -> dict[str, int]
Viết hàm đếm số lần xuất hiện của mỗi nguyên âm (a, e, i,
o, u) trong chuỗi s (case-insensitive). Trả dict có 5 key
tương ứng, kể cả khi count = 0.
count_vowels("Hello World")
# {'a': 0, 'e': 1, 'i': 0, 'o': 2, 'u': 0}
Implement 2 cách: (a) manual dict + loop, (b) dùng Counter.
Đáp án
from collections import Counter
# Cách (a) — manual dict + loop
def count_vowels(s: str) -> dict[str, int]:
result = {v: 0 for v in "aeiou"}
for c in s.lower():
if c in result:
result[c] += 1
return result
# Cách (b) — Counter
def count_vowels_v2(s: str) -> dict[str, int]:
c = Counter(ch for ch in s.lower() if ch in "aeiou")
return {v: c[v] for v in "aeiou"} # đảm bảo có đủ 5 key
Counter trả 0 khi tra key vắng nên c[v] không lỗi. Generator
expression bên trong Counter() tránh tạo list trung gian.
Bài 2 — group_by_first_letter(words) -> dict[str, list[str]]
Gom danh sách từ thành dict theo ký tự đầu (chữ thường):
group_by_first_letter(["apple", "ant", "bat", "banana", "cat"])
# {'a': ['apple', 'ant'], 'b': ['bat', 'banana'], 'c': ['cat']}
Dùng defaultdict(list).
Đáp án
from collections import defaultdict
def group_by_first_letter(words: list[str]) -> dict[str, list[str]]:
groups = defaultdict(list)
for w in words:
groups[w[0].lower()].append(w)
return dict(groups) # trả dict thường, không lộ defaultdict ra ngoài
# Không có defaultdict, bạn sẽ phải:
# - Check `if k not in groups: groups[k] = []`
# - Hoặc dùng setdefault: groups.setdefault(k, []).append(w)
# Cả hai dài hơn defaultdict.
Bài 3 — Unique sorted desc
Cho list [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]. Tạo list mới chứa các phần tử
unique, sắp xếp giảm dần. Dùng comprehension + set + sorted —
không loop tay.
Đáp án
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# Ngắn gọn nhất — dedupe bằng set, sort giảm dần
result = sorted(set(nums), reverse=True)
# [9, 6, 5, 4, 3, 2, 1]
# Tương đương với set comprehension:
result_v2 = sorted({x for x in nums}, reverse=True)
# Hoặc nếu chỉ muốn comprehension (vẫn dùng set bên trong):
seen = set()
result_v3 = sorted(
[x for x in nums if not (x in seen or seen.add(x))],
reverse=True,
)
# Trick: seen.add(x) trả None (falsy) — nên expression cuối luôn là (x in seen)
Pythonic = dùng set + sorted(reverse=True). Ngắn, đọc rõ ý.
Bài 4 — flatten(lst) N-level
Cho list lồng nhiều cấp (không biết độ sâu), trả list 1 cấp chứa mọi phần tử leaf. Ví dụ:
flatten([1, [2, [3, [4, [5]]], 6])
# [1, 2, 3, 4, 5, 6]
Viết version đệ quy. Bonus: viết bằng comprehension lồng nhau.
Đáp án
# Version 1 — đệ quy explicit
def flatten(lst):
result = []
for x in lst:
if isinstance(x, list):
result.extend(flatten(x)) # gọi đệ quy
else:
result.append(x)
return result
# Version 2 — generator (lazy, đẹp hơn)
def flatten_gen(lst):
for x in lst:
if isinstance(x, list):
yield from flatten_gen(x) # yield from = chuyển tiếp generator
else:
yield x
list(flatten_gen([1, [2, [3, [4]]]])) # [1, 2, 3, 4]
# Version 3 — comprehension 1 cấp (KHÔNG đệ quy được trong comprehension)
# Chỉ flatten 1 cấp:
flatten_1 = lambda lst: [x for sub in lst for x in sub]
yield from là Python 3.3+ — chuyển tiếp toàn bộ giá trị từ sub-generator. Đẹp
hơn for x in flatten_gen(...): yield x.
Bài 5 — Sliding window windows(arr, size)
Trả về list các sub-list kích thước cố định size, di chuyển 1 bước mỗi lần.
windows([1, 2, 3, 4], 2)
# [[1, 2], [2, 3], [3, 4]]
windows([1, 2, 3, 4, 5], 3)
# [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
Viết 2 version: (a) dùng index + slicing, (b) dùng deque(maxlen=size). So sánh.
Đáp án
# (a) Slicing — đơn giản nhất
def windows(arr, size):
return [arr[i:i+size] for i in range(len(arr) - size + 1)]
# (b) deque — efficient với stream / iterable không có len()
from collections import deque
def windows_deque(arr, size):
it = iter(arr)
dq = deque(maxlen=size)
result = []
for x in it:
dq.append(x)
if len(dq) == size:
result.append(list(dq)) # copy ra list — deque sẽ bị mutate ở lần lặp sau
return result
# So sánh:
# - Slicing: O(n*size) total, ngắn, đọc rõ — đủ cho list trong memory.
# - Deque: O(n) total, dùng được với iterable lazy (file lines, network stream).
# Python 3.10+ có itertools.pairwise(arr) — windows size 2 built-in.
Quiz
{} tạo ra gì? Làm sao tạo set rỗng?
Xem đáp án
{} tạo dict rỗng, KHÔNG phải set rỗng (vì dict ra đời trước, đã
"chiếm" cú pháp này). Set rỗng phải dùng set(). Còn {1, 2} mới là set
literal (có ít nhất một phần tử). Đây là một trong những gotcha nhớ đời với người mới Python.
(1) và (1,) khác gì?
Xem đáp án
(1) là int 1 — dấu ngoặc ở đây chỉ là dấu nhóm biểu thức (như toán
học). (1,) mới là tuple 1 phần tử. Trong Python, dấu phẩy
là cái tạo tuple, không phải dấu ngoặc. Vì vậy 1, 2, 3 không có ngoặc vẫn là tuple.
Cũng vì lý do này: type(()) = tuple (tuple rỗng), type((1)) = int.
List comprehension và generator expression khác gì?
Xem đáp án
Cú pháp: list comp dùng [...], gen expr dùng (...).
Hành vi: list comp là eager — tính toàn bộ và lưu trong RAM ngay. Gen
expr là lazy — chỉ lưu state, sinh từng phần tử khi được iterate. Vì vậy với dữ liệu
lớn (hoặc infinite), gen expr tiết kiệm memory. Đổi lại, gen expr chỉ duyệt được 1
lần; sau đó cạn. List có thể duyệt lại tuỳ ý.
Vì sao set lookup nhanh hơn list?
Xem đáp án
set implement bằng hash table: lookup là O(1) trung bình — tính
hash(x), nhảy đến bucket tương ứng, so sánh. list phải duyệt tuần tự từng phần tử
cho đến khi tìm thấy (hoặc hết) — O(n). Với danh sách lớn, sự khác biệt có thể lên đến hàng
nghìn lần. Khi cần check membership lặp lại (đặc biệt trong vòng lặp), luôn convert sang
set: seen = set(big_list).
d.get('key') và d['key'] khác gì?
Xem đáp án
d['key'] raise KeyError nếu key không tồn tại. d.get('key')
trả None (hoặc default tự chọn) — không lỗi. Dùng d['key'] khi bạn
chắc chắn key có (lỗi sớm tốt — fail fast). Dùng d.get('key', default) khi
key có thể vắng và bạn có giá trị fallback (ví dụ: load config, parse JSON optional field).
Có thể dùng list làm dict key không? Tuple thì sao?
Xem đáp án
Không với list — list mutable, không hashable, raise
TypeError: unhashable type: 'list'. Được với tuple, miễn là mọi
phần tử bên trong tuple cũng hashable. Ví dụ (1, 2) OK; (1, [2]) không
(vì chứa list bên trong). Nếu cần set/dict key biểu diễn "danh sách phần tử bất kỳ thứ tự",
dùng frozenset thay tuple.
sorted(lst) và lst.sort() khác gì?
Xem đáp án
sorted(lst) là built-in function, không mutate, trả list mới
đã sort. Áp dụng được cho mọi iterable (tuple, set, generator) — luôn trả
list. lst.sort() là method của list, mutate in-place, trả
None. Quy tắc Python: method mutate thì không trả value (để tránh chain nhầm như
x = lst.sort()). Cả hai chấp nhận key=, reverse=.
Vì sao Python 3.7+ guarantee dict ordered?
Xem đáp án
Từ CPython 3.6, implementation của dict được viết lại (compact dict design của
Raymond Hettinger) — vô tình bảo toàn insertion order như effect phụ. Đến Python
3.7, điều này được nâng lên thành language spec chính thức (PEP không cụ thể —
quyết định ở mailing list / dev meeting), có nghĩa là mọi implementation của Python
(CPython, PyPy, MicroPython...) phải bảo toàn insertion order. Trước 3.6, thứ tự là không xác
định — phải dùng collections.OrderedDict. Bây giờ OrderedDict vẫn còn,
chỉ dùng khi cần method đặc biệt như move_to_end().
Tổng kết
Sau chương 3, bạn nên đã master:
- 4 cấu trúc built-in: list (mutable sequence), tuple (immutable sequence + hashable), dict (hash map, ordered từ 3.7), set (unique unordered).
- frozenset: immutable set, hashable, dùng được làm dict key / set element.
- Hashable: int/float/bool/str/tuple-of-hashable/frozenset/None hashable; list/dict/set không. Mutable thì không hashable.
- Tuple unpacking: swap
a, b = b, a; return nhiều giá trị; star unpackfirst, *rest; nested. - Slicing
[start:stop:step]: reverse[::-1], step[::2], slice không bao giờ raise IndexError. - Comprehension: list / dict / set / generator — Pythonic, ngắn, đẹp. Nested + filter.
- Generator expression: lazy, tiết kiệm RAM, idiom
sum(x for x in ...). - collections stdlib:
Counter(đếm tần suất),defaultdict(auto-init),deque(queue O(1) 2 đầu),namedtuple(tuple có tên). - Gotcha:
{}là dict rỗng (set rỗng =set());(1)là int;list.sort()trảNone;[[0]*3]*3chia sẻ reference.
Kết nối
- Chương 4 (Control Flow & Functions) — comprehension là syntactic sugar cho
forloop;matchstatement (3.10+) destructure được dict/tuple. - Chương 5 (OOP in Python) —
@dataclasslà phiên bản hiện đại của namedtuple, có method, default, validation. - Chương 6 (Iterators & Generators) — đào sâu generator expression,
yield,itertools(chain, groupby, accumulate, pairwise). - Chương 9 (Typing) — type hints cho
list[int],dict[str, list[str]],tuple[int, ...],TypedDict,NamedTuple. - Dart Chương 6 (Collections) — đối chiếu
List/Set/MapDart,constcollection, spread operator. Dart không có comprehension built-in nhưng có collection-if + collection-for tương tự. - JavaScript Chương 5 (Arrays & Iterables) — JS không có comprehension; phải dùng
map/filter/reduce. Set/Map JS gần giống Python nhưng key bất kỳ.
— Zen of Python (PEP 20)
Trong chương này, comprehension chính là "the one obvious way" cho việc biến đổi collection.
Khi đã quen, bạn sẽ thấy loop + append rất dài dòng so với 1 dòng comprehension. Đó là
Pythonic.