Bạn viết một vòng lặp đếm số lần xuất hiện của từ, phải nhớ kiểm tra key đã có chưa. Bạn gom nhân viên theo phòng ban, lại thêm một lần kiểm tra nữa. Module collections trong thư viện chuẩn có sẵn bốn kiểu dữ liệu xóa hết những dòng lặt vặt đó.
Counter: đếm tần suất
Tình huống thật: bạn có file log và muốn biết địa chỉ IP nào gọi nhiều nhất. Cách làm bằng dict thuần trông như thế này:
dem = {}
for ip in danh_sach_ip:
if ip in dem:
dem[ip] += 1
else:
dem[ip] = 1
Với Counter thì chỉ còn một dòng, và bạn được thêm hàm sắp xếp sẵn:
from collections import Counter
tu = "mot hai ba hai ba ba".split()
dem = Counter(tu)
print(dem) # Counter({'ba': 3, 'hai': 2, 'mot': 1})
print(dem.most_common(2)) # [('ba', 3), ('hai', 2)]
print(sum(dem.values())) # 6 — tổng số phần tử
Điểm khác biệt quan trọng so với dict: hỏi một key chưa có thì Counter trả về 0 chứ không ném KeyError.
print(dem["khong_co"]) # 0, không lỗi
Counter còn cộng trừ được với nhau, rất tiện khi gộp thống kê từ nhiều nguồn:
print(Counter("aab") + Counter("abc")) # Counter({'a': 3, 'b': 2, 'c': 1})
print(Counter("aaab") - Counter("ab")) # Counter({'a': 2})
# Đếm ký tự trong chuỗi cũng chỉ một dòng
print(Counter("khanh").most_common(3)) # [('h', 2), ('k', 1), ('a', 1)]
defaultdict: gom nhóm không cần kiểm tra key
Tình huống thật: bạn có danh sách nhân viên và muốn gom theo phòng ban. Bằng dict thuần, mỗi vòng lặp phải kiểm tra xem key đã có list chưa:
nhom = {}
for ten, phong in nhan_vien:
if phong not in nhom:
nhom[phong] = []
nhom[phong].append(ten)
defaultdict nhận vào một hàm tạo giá trị mặc định, và tự gọi hàm đó khi gặp key mới:
from collections import defaultdict
nhom = defaultdict(list)
for ten, phong in [("An", "KD"), ("Bình", "KT"), ("Cường", "KT")]:
nhom[phong].append(ten)
print(dict(nhom)) # {'KD': ['An'], 'KT': ['Bình', 'Cường']}
Đổi kiểu mặc định là đổi luôn công dụng:
tong = defaultdict(int) # mặc định 0, dùng để cộng dồn
for tu in danh_sach:
tong[tu] += 1
duy_nhat = defaultdict(set) # mặc định set, dùng để gom không trùng
duy_nhat["x"].add(1)
duy_nhat["x"].add(1)
print(dict(duy_nhat)) # {'x': {1}}
Một lưu ý: chỉ cần đọc một key chưa có là defaultdict đã lặng lẽ tạo ra nó. Nếu chỉ muốn tra cứu mà không muốn dict phình ra, hãy dùng dict.get(key, mac_dinh). Khi in ra hay lưu vào JSON, bọc dict() cho gọn.
deque: hàng đợi nhanh ở cả hai đầu
Tình huống thật: bạn xử lý hàng đợi công việc, lấy ra ở đầu danh sách. Với list, lst.pop(0) phải dời toàn bộ phần tử còn lại lên một ô — danh sách càng dài càng chậm. deque thêm và lấy ở cả hai đầu với chi phí như nhau.
from collections import deque
hang_doi = deque([1, 2, 3])
hang_doi.appendleft(0) # thêm vào đầu
hang_doi.append(4) # thêm vào cuối
print(list(hang_doi)) # [0, 1, 2, 3, 4]
print(hang_doi.popleft()) # 0 — lấy ra từ đầu
print(hang_doi.pop()) # 4 — lấy ra từ cuối
Công dụng thứ hai, và có lẽ hay dùng hơn: giữ đúng N phần tử gần nhất. Tham số maxlen tự đẩy phần tử cũ ra khi đầy.
# Chỉ giữ 3 giá trị mới nhất
lich_su = deque(maxlen=3)
for i in range(6):
lich_su.append(i)
print(list(lich_su)) # [3, 4, 5]
Rất hợp để giữ 100 dòng log cuối, 10 lệnh vừa gõ, hay cửa sổ trượt khi tính trung bình động — bạn không phải tự viết code cắt bớt đầu danh sách. Ngoài ra rotate xoay vòng các phần tử:
d = deque([1, 2, 3, 4, 5])
d.rotate(2)
print(list(d)) # [4, 5, 1, 2, 3]
namedtuple: tuple mà các trường có tên
Tình huống thật: hàm của bạn trả về (3, 4) và sáu tháng sau không ai nhớ số nào là x, số nào là y. Truy cập bằng kq[0] thì đúng nhưng chẳng nói lên điều gì.
from collections import namedtuple
Diem = namedtuple("Diem", ["x", "y"])
p = Diem(3, 4)
print(p) # Diem(x=3, y=4) — in ra là đọc hiểu ngay
print(p.x) # 3 — theo tên
print(p[0]) # 3 — vẫn theo chỉ số như tuple thường
Nó vẫn là tuple thật: bất biến, giải nén được, dùng làm key của dict được, tốn bộ nhớ ngang tuple chứ không như một object thường. Vài tiện ích đi kèm:
print(Diem._fields) # ('x', 'y')
print(p._asdict()) # {'x': 3, 'y': 4} — tiện xuất JSON
print(p._replace(y=99)) # Diem(x=3, y=99) — tạo bản mới, p không đổi
NhanVien = namedtuple("NhanVien", "ten tuoi phong", defaults=["Chưa rõ"])
print(NhanVien("An", 30)) # NhanVien(ten='An', tuoi=30, phong='Chưa rõ')
Các giá trị trong defaults gán từ phải sang trái, nên chỉ những trường cuối mới có mặc định. Vì bất biến nên p.x = 10 sẽ báo lỗi — đó là điểm mạnh khi bạn muốn dữ liệu không bị sửa nhầm, và là điểm yếu khi bạn cần thay đổi thường xuyên.
Chọn cái nào?
- Cần biết cái gì xuất hiện bao nhiêu lần, hoặc top N —
Counter. - Cần gom nhóm hoặc cộng dồn theo key —
defaultdict. - Cần hàng đợi, hoặc chỉ giữ N phần tử gần nhất —
deque. - Cần một bản ghi nhỏ, bất biến, trả về từ hàm —
namedtuple.
Vài nguyên tắc nên nhớ
- Thấy mình viết
if key not in dtrong vòng lặp là dấu hiệu nên đổi sangdefaultdicthoặcCounter. - Thấy
lst.pop(0)trên danh sách dài là dấu hiệu nên đổi sangdeque. - Hàm trả về nhiều hơn hai giá trị thì đặt tên cho chúng bằng
namedtuple. defaultdicttạo key ngay khi bạn chỉ đọc — cần tra cứu thuần thì dùng.get().
Cả bốn kiểu này đều có sẵn trong thư viện chuẩn, không cần cài gì thêm. Chỉ cần nhớ chúng tồn tại là code của bạn ngắn đi vài dòng mỗi lần.
