Dùng itertools để ‘xử đẹp’ dữ liệu lớn: Đừng để MemoryError làm phiền bạn

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Khi RAM ‘đầu hàng’ trước dữ liệu lớn

Bạn đã bao giờ treo máy khi chạy một script Python xử lý file CSV vài GB chưa? Cảm giác nhìn dòng chữ MemoryError đỏ chót hiện lên sau 15 phút chờ đợi thực sự rất ức chế.

Mình từng nếm trải việc này khi xử lý 1 triệu dòng log hệ thống. Ban đầu, mình chọn cách đơn giản nhất: read() toàn bộ vào một list rồi mới xử lý. Kết quả là thanh RAM 16GB bị nuốt gọn trong tích tắc. Đây là cái bẫy kinh điển mà nhiều lập trình viên mắc phải khi cố nhồi nhét mọi thứ vào bộ nhớ cùng lúc.

Tại sao cách làm truyền thống lại ngốn RAM?

Vấn đề nằm ở cơ chế Eager Evaluation (Tính toán tức thời). Thử tạo một list với 10 triệu con số bằng [x for x in range(10000000)], Python sẽ lập tức đòi cấp phát khoảng 400MB RAM chỉ để giữ chỗ cho danh sách đó.

Thực tế, chúng ta thường chỉ cần xử lý từng phần tử một tại mỗi thời điểm. Việc bắt Python phải “nhớ” toàn bộ 10 triệu phần tử là một sự lãng phí tài nguyên không cần thiết. Với các bài toán tổ hợp hay quét database lớn, cách làm này chắc chắn sẽ khiến hệ thống sập nguồn.

Giải pháp: Tư duy ‘làm đến đâu, tính đến đó’ với itertools

Thư viện itertools sinh ra để giải quyết bài toán này bằng cơ chế Lazy Evaluation (Tính toán trì hoãn). Thay vì trả về một danh sách đầy đủ, nó cung cấp một iterator. Hãy coi iterator như một dây chuyền sản xuất: nó chỉ tạo ra sản phẩm tiếp theo khi bạn thực sự nhấn nút next() hoặc gọi trong vòng lặp for.

Dưới đây là 3 nhóm công cụ trong itertools giúp code của mình chạy mượt hơn hẳn.

1. Nhóm vòng lặp vô hạn (Infinite Iterators)

Thay vì dùng while True kèm biến đếm thủ công dễ gây lỗi, itertools giúp code sạch và tường minh hơn.

import itertools

# count(start, step): Tạo dãy số vô hạn, cực tiện để đánh ID cho object
for i in itertools.count(10, 5):
    if i > 30: break
    print(i)  # Kết quả: 10, 15, 20, 25, 30

# cycle(iterable): Lặp lại vòng tròn các phần tử
# Ứng dụng: Gán luân phiên màu sắc cho các dòng trong bảng (Striped rows)
colors = ['Red', 'Green', 'Blue']
for color in itertools.islice(itertools.cycle(colors), 6):
    print(color)

2. Xử lý chuỗi dữ liệu mà không cần tạo list phụ

Đây là bí kíp giúp mình giữ cho mức chiếm dụng RAM luôn ở mức thấp, bất kể dữ liệu đầu vào lớn đến đâu.

itertools.chain: Kết hợp nhiều danh sách thành một luồng duy nhất. Bạn sẽ không tốn thêm byte RAM nào để tạo ra một list trung gian mới.

list_a = range(1000000)
list_b = range(1000000)

# Cách tệ: combined = list_a + list_b (Tạo list mới 2 triệu phần tử)
# Cách tốt: 
for item in itertools.chain(list_a, list_b):
    # Xử lý trực tiếp từng phần tử
    pass

itertools.islice: Cắt (slice) dữ liệu từ một iterator. Điểm cộng lớn nhất là nó không cần load toàn bộ dữ liệu vào bộ nhớ để lấy index.

# Đọc nhanh 5 dòng đầu của một file log 20GB
with open('huge_log.txt', 'r') as f:
    first_five = itertools.islice(f, 5)
    for line in first_five:
        print(line.strip())

3. Tổ hợp và Chỉnh hợp – Không còn nỗi lo đệ quy

Khi cần sinh mã SKU cho sản phẩm từ các thuộc tính như màu sắc (5 màu), kích cỡ (6 size), chất liệu (4 loại), mình luôn dùng itertools.product. Nó nhanh và sạch hơn nhiều so với việc viết 3-4 vòng for lồng nhau.

# Sinh 120 tổ hợp SKU chỉ với 1 dòng code
variants = itertools.product(['S', 'M', 'L'], ['Red', 'Blue'], ['Cotton', 'Silk'])

Nếu bạn tự viết thuật toán đệ quy để tìm tổ hợp (combinations), code sẽ rất chậm khi tập dữ liệu lớn. itertools.combinations được tối ưu ở tầng C, giúp xử lý các bài toán xác suất nhanh hơn gấp nhiều lần.

Kinh nghiệm thực tế: Đừng lạm dụng!

Dù mạnh mẽ, không phải lúc nào bạn cũng cần đến itertools. Với danh sách chỉ vài trăm phần tử, dùng list truyền thống sẽ dễ debug và trực quan hơn.

Hãy chuyển sang itertools khi:

  • Dữ liệu đầu vào lớn hơn 50% dung lượng RAM máy tính.
  • Bạn đang xây dựng data pipeline cần tốc độ phản hồi nhanh (real-time).
  • Cần thực hiện các phép toán tổ hợp với số lượng phần tử đầu vào n > 15.

Có lần mình xử lý 100.000 records từ API. Thay vì gom tất cả vào một mảng lớn, mình bọc kết quả vào generator và dùng islice để chia batch nhỏ 1.000 dòng. Cách này giúp biểu đồ RAM luôn là một đường thẳng tắp, không hề có các đỉnh nhọn (spike) gây nguy hiểm cho server.

Làm chủ itertools không chỉ giúp bạn viết code chuyên nghiệp hơn. Nó thay đổi hoàn toàn cách bạn tư duy về quản lý tài nguyên, giúp ứng dụng Python của bạn bền bỉ hơn trước những luồng dữ liệu khổng lồ.

Share: