Học bằng cách đọc theo thứ tự

collections — Counter / defaultdict / deque / namedtuple

Học Counter để đếm số lần xuất hiện, defaultdict để xử lý khóa thiếu, deque cho thao tác O(1) ở hai đầu và ring buffer, cùng namedtuple cho bản ghi có tên qua ví dụ thật.

Module collections tập hợp các cấu trúc dữ liệu chuyên biệt lấp những khoảng trống mà list / dict / tuple để lại. Bài viết này sẽ đi qua bốn cấu trúc bạn dùng nhiều nhất trong dự án thực tế, theo thứ tự: Counter / defaultdict / deque / namedtuple.

Bốn cấu trúc này dùng để làm gì

Mỗi cấu trúc tồn tại để đơn giản hóa một tình huống vốn khó viết với built-in thuần. Bảng dưới cho bạn cái nhìn tổng quan, sau đó từng phần sẽ đi sâu vào cách dùng.

Bốn kiểu trong collections
CounterĐếm số lần xuất hiệndefaultdictTự khởi tạo khóa thiếudequeO(1) ở cả hai đầunamedtupleĐặt tên cho field tuple
Counter dùng để đếm, defaultdict xử lý khóa thiếu, deque cho thao tác nhanh ở cả hai đầu, và namedtuple thêm tên cho tuple. Cả bốn đều là kiểu mở rộng xây trên list / dict / tuple thuần để bổ sung tiện ích.
KiểuVấn đề giải quyếtCách thay thế thuần
CounterĐếm phần tử từng cái bằng vòng lặp khá dài dòngfor + dict đếm
defaultdictPhải viết if x not in d: d[x] = ... trước khi dùng khóa mớidict.setdefault(...)
dequelist.insert(0, ...) và list.pop(0) chậm (O(n))list (ổn cho dữ liệu nhỏ)
namedtuplePhần tử tuple chỉ truy cập được theo vị trídataclass hoặc dict (nặng hơn)

Counter — đếm số lần xuất hiện trong một dòng

Counter là class nhận một iterable (list, chuỗi, v.v.) và trả về dict đếm số lần xuất hiện của từng phần tử. Với dict thuần, bạn phải viết vòng lặp kiểm tra khóa đã tồn tại chưa, khởi tạo bằng 1 nếu chưa có, hoặc tăng lên nếu đã có — nhưng Counter làm cùng việc đó trong một dòng: Counter(list).

Giá trị trả về là subclass của dict, nên bạn có thể lấy giá trị bằng counter["apple"] y như dict bình thường. Còn có .most_common(N) trả về list các tuple (phần tử, số lần) theo thứ tự giảm dần — rất hợp để hiển thị bảng xếp hạng.

Counter hoạt động thế nào
List[apple, banana, apple, ...]Counter(list)Counter{apple: 3, banana: 2, ...}đếm
Chỉ cần truyền một iterable và bạn nhận lại subclass của dict với số lần xuất hiện theo từng phần tử. most_common(N) trả về top N theo tần suất, nên bạn có thể viết bảng xếp hạng trong một dòng.

Đếm sáu bản ghi mua hàng theo sản phẩm bằng Counter. Cảm nhận cách một dòng thay thế cho vòng lặp for thông thường.

① Hãy import Counter từ collections

② Hãy định nghĩa list mua hàng ["apple", "banana", "apple", "cherry", "banana", "apple"]

③ Hãy truyền list cho Counter để đếm rồi in ra dưới dạng Đếm: ◯

④ Hãy in số lượng apple dưới dạng Số lượng apple: ◯ (có thể dùng truy cập bằng dấu ngoặc vuông như dict)

⑤ Hãy dùng most_common để lấy top 2 rồi in ra dưới dạng Top 2: ◯

(Nếu code chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

defaultdict — tự khởi tạo khóa thiếu

defaultdict là dict tự động chèn giá trị mặc định khi bạn truy cập một khóa chưa tồn tại. Với dict thuần, mỗi lần bạn phải viết kiểm tra tồn tại + khởi tạo, kiểu if key not in d: d[key] = []. Với defaultdict, bạn truyền một hàm sinh giá trị khởi tạo lúc tạo, và nó tự gọi hàm đó khi lần đầu chạm tới khóa thiếu.

Hai pattern bạn gặp nhiều nhất là defaultdict(list) (list rỗng cho khóa thiếu) và defaultdict(int) (0 cho khóa thiếu). Đối số là một hàm trả về giá trị khởi tạo khi gọi không có đối sốlistint đều phù hợp (chúng trả về []0), nên truyền trực tiếp là được.

dict so với defaultdict
dict thuầnd["x"] (không có khóa)→ KeyErrorCần:if x not in d: d[x] = []defaultdict(list)d["x"] (không có khóa)→ Tự tạo []Gọi .append(...)trực tiếp
dict thuần raise KeyError khi khóa thiếu. defaultdict gọi hàm bạn truyền lúc tạo và dùng kết quả. Truyền list cho list rỗng, int cho 0, set cho set rỗng.

Khi nào chọn Counter và defaultdict

Nếu bạn chỉ muốn đếm, Counter ngắn gọn hơn (và bạn có sẵn most_common). Nếu bạn gom thành list hoặc set — bất cứ tình huống nào giá trị khởi tạo là container rỗngdefaultdict(list) / defaultdict(set) là lựa chọn phù hợp hơn.

Gom các loại quả theo màu. Với defaultdict(list), lần đầu bạn chạm vào một khóa màu, nó tự tạo một list rỗng, nên bạn có thể gọi .append mà không cần kiểm tra tồn tại.

① Hãy import defaultdict từ collections

② Hãy tạo defaultdict có giá trị mặc định là list rỗng

③ Hãy đăng ký ba mục theo thứ tự:

- "red""apple"

- "yellow""banana"

- "red""cherry"

④ Hãy chuyển sang dict thường rồi in dưới dạng Theo màu: ◯ (in defaultdict trực tiếp cho ra repr khó đọc hơn)

Python Editor

Chạy code để xem đầu ra

deque — thao tác O(1) ở cả hai đầu

deque (double-ended queue) là cấu trúc dữ liệu giống list, hỗ trợ thêm và xóa từ cả hai đầu. list thuần đã xử lý thao tác đầu phải qua appendpop, nhưng list.insert(0, x) và list.pop(0) phải dịch toàn bộ phần tử đi một ô bên trong, nên hóa ra là O(n) (chậm dần khi list lớn).

deque được thiết kế để cả hai đầu đều O(1) (thời gian hằng số bất kể độ dài), nên là lựa chọn đúng cho hàng đợi, buffer lịch sử, và giữ N mục gần nhất — bất cứ chỗ nào bạn cần push và pop từ cả hai phía. Đối số maxlen đặc biệt tiện: đặt độ dài tối đa, và các lần thêm vượt quá giới hạn lặng lẽ đẩy phần tử ra ở đầu đối diện, cho bạn ring buffer miễn phí.

Chi phí thao tác list so với deque
listThêm / xóa đầu tráiDịch mọi phần tửđi một ô→ O(n) chậmdequeappend / pop hai đầuChỉ thao táccon trỏ→ O(1) nhanh
Thao tác đầu trái của list là O(n) (mọi phần tử bị dịch), trong khi deque là O(1) ở cả hai đầu. Với deque(maxlen=N), bạn có ring buffer tự đẩy ra cái cũ nhất khi đầy.
Phương thứcHiệu ứngChi phí
append(x)Thêm vào đầu phảiO(1)
appendleft(x)Thêm vào đầu tráiO(1)
pop()Xóa từ đầu phảiO(1)
popleft()Xóa từ đầu tráiO(1)
maxlen=NĐặt độ dài tối đa (ring buffer)Đầu đối diện tự rớt ra
Các phương thức cốt lõi của deque
appendleft(x)deque[a, b, c]append(x)popleft()pop()
Đối với deque ở giữa, đầu trái dùng appendleft / popleftđầu phải dùng append / pop. Hướng mũi tên thể hiện thêm (vào deque)xóa (ra khỏi deque).

Giữ 3 access log gần nhất trong một deque(maxlen=3). Sau 5 lần append, chỉ 3 mục cuối còn lại.

① Hãy import deque từ collections

② Hãy tạo deque rỗng với độ dài tối đa 3

③ Hãy append các số nguyên 0 đến 4 theo thứ tự (5 phần tử)

④ Hãy in kết quả dưới dạng 3 cuối: ◯ (01 cũ hơn đã bị đẩy ra)

Python Editor

Chạy code để xem đầu ra

Pop đầu và cuối của hàng đợi tác vụ. Dùng popleftpop để chuyển đổi đầu nào bạn lấy ra.

① Hãy import deque từ collections

② Hãy xây deque từ bốn tác vụ ["Tác vụ A", "Tác vụ B", "Tác vụ C", "Tác vụ D"]

③ Hãy dùng popleft() để lấy tác vụ đầu và in dưới dạng Tác vụ đầu: ◯

④ Hãy dùng pop() để lấy tác vụ cuối và in dưới dạng Tác vụ cuối: ◯

⑤ Hãy in deque còn lại dưới dạng Còn lại: ◯

Python Editor

Chạy code để xem đầu ra

namedtuple — bản ghi nhẹ đặt tên cho field tuple

namedtuple là hàm định nghĩa tuple có tên field trong một dòng. Tuple thuần như (3, 4) chỉ truy cập được theo vị trí (p[0] / p[1]), nên người đọc phải nhớ ô nào là gì. namedtuple cho phép viết p.x / p.y với tên có ý nghĩa, đây là điểm cộng lớn về khả năng đọc.

Nó cũng tương thích với tuple thông thường — index p[0] và unpack *p vẫn hoạt động — nên bạn có thể thêm tên vào code đã có dùng tuple mà không phá vỡ gì. Một mở rộng nhẹ nhàng.

namedtuple nhìn nhanh
Point = namedtuple('Point', ['x', 'y'])p = Point(3, 4)Theo tênp.x / p.yTheo chỉ mụcp[0] / p[1]Dạng dictp._asdict()
Định nghĩa tương đương class trong một dòng, rồi dùng cả truy cập theo vị trí (p[0]) và truy cập theo tên (p.x). _asdict() chuyển sang dict, hợp với JSON và pprint.

namedtuple so với dataclass

Nếu bạn chỉ cần một bản ghi nhẹ, bất biến, hãy chọn namedtuple. Nếu bạn muốn phương thức, giá trị mặc định, và type hint chi tiết, dataclass (giới thiệu trong bài tiếp theo) là công cụ phù hợp. Điểm mạnh của namedtuple là tương thích với tuple — hoàn hảo để thay thế kiểu trả về như def f() -> tuple[int, int]: mà không phá vỡ caller.

Định nghĩa một namedtuple Point và đọc tọa độ theo tên.

① Hãy import namedtuple từ collections và tạo một namedtuple với tên class "Point" và các field ["x", "y"]

② Hãy tạo instance p với x=3, y=4

③ Hãy in dưới dạng x: 3 y: 4 dùng truy cập theo tên p.xp.y

Python Editor

Chạy code để xem đầu ra

Tính năng phụ của namedtuple — repr / index / _asdict

Bên cạnh truy cập theo tên kiểu class, namedtuple cho bạn truy cập index tương thích tuple, repr dễ đọc, và phương thức _asdict để chuyển sang dict. Bạn sẽ dùng những thứ này cho debug, tương thích với API có sẵn, và serialize JSON, cùng nhiều mục đích khác.

Lấy p từ Thực hành 1 và thử ba pattern truy cập phụ.

print("repr:", p) để in dưới dạng repr: Point(x=3, y=4)

② Dùng p[0]p[1] để truy cập theo chỉ mục rồi in dưới dạng Theo chỉ mục: 3 4

③ Dùng p._asdict() để in dưới dạng Dạng dict: {'x': 3, 'y': 4}

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Cách ngắn gọn nhất để đếm số lần xuất hiện theo từng phần tử của list nums trong một dòng là gì?

Câu 2Sau khi tạo defaultdict(list), lần truy cập đầu tiên vào khóa mới d["x"] cho bạn cái gì?

Câu 3Cái nào hợp nhất khi bạn muốn chỉ giữ 5 mục gần nhất?

Câu 4Điểm mạnh chính của namedtuple so với tuple thuần là gì?