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ì?
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.
list / dict / tuple thuần để bổ sung tiện ích.| Kiểu | Vấn đề giải quyết | Cách thay thế thuần |
|---|---|---|
| Counter | Đếm phần tử từng cái bằng vòng lặp khá dài dòng | for + dict đếm |
| defaultdict | Phải viết if x not in d: d[x] = ... trước khi dùng khóa mới | dict.setdefault(...) |
| deque | list.insert(0, ...) và list.pop(0) chậm (O(n)) | list (ổn cho dữ liệu nhỏ) |
| namedtuple | Phầ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.
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.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ố — list và int đều phù hợp (chúng trả về [] và 0), nên truyền trực tiếp là được.
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ỗng — defaultdict(list) / defaultdict(set) là lựa chọn phù hợp hơn.
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 append và pop, 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í.
deque(maxlen=N), bạn có ring buffer tự đẩy ra cái cũ nhất khi đầy.| Phương thức | Hiệu ứng | Chi phí |
|---|---|---|
| append(x) | Thêm vào đầu phải | O(1) |
| appendleft(x) | Thêm vào đầu trái | O(1) |
| pop() | Xóa từ đầu phải | O(1) |
| popleft() | Xóa từ đầu trái | O(1) |
| maxlen=N | Đặt độ dài tối đa (ring buffer) | Đầu đối diện tự rớt 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.
_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.
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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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ì?