Python Generator: Xử Lý Hàng Triệu Dòng Dữ Liệu Mà Không Tốn RAM

  • Home
  • Python Generator: Xử Lý Hàng Triệu Dòng Dữ Liệu Mà Không Tốn RAM

Khi làm việc với dữ liệu lớn trong Python, một trong những vấn đề phổ biến nhất là RAM bị tiêu tốn quá nhiều.

Ví dụ, bạn có một file CSV chứa vài triệu dòng dữ liệu. Cách đơn giản nhất là đọc toàn bộ file vào một list:

with open("users.txt", "r", encoding="utf-8") as file:
    users = file.readlines()

for user in users:
    print(user)

Cách này hoạt động tốt với file nhỏ. Nhưng nếu file có hàng triệu dòng, toàn bộ dữ liệu sẽ được đưa vào RAM cùng lúc.

Đây chính là lúc Python Generator trở nên cực kỳ hữu ích.

Python Generator là gì?

Generator là một cơ chế cho phép Python tạo dữ liệu từng phần thay vì tạo toàn bộ dữ liệu cùng một lúc.

Thay vì lưu hàng triệu phần tử trong bộ nhớ, Generator chỉ tạo ra phần tử tiếp theo khi chương trình cần.

Ví dụ:

def numbers():
    for i in range(10):
        yield i

Sử dụng:

for number in numbers():
    print(number)

Điểm quan trọng nằm ở từ khóa:

yield

Khác với return, yield không kết thúc hoàn toàn function. Nó tạm dừng function và trả về một giá trị.

Khi vòng lặp yêu cầu giá trị tiếp theo, function tiếp tục chạy từ vị trí đã dừng.


Generator khác List như thế nào?

Hãy xem hai cách tạo một dãy số:

numbers = [i for i in range(1000000)]

và:

numbers = (i for i in range(1000000))

Cách đầu tiên tạo toàn bộ 1 triệu số và lưu chúng trong bộ nhớ.

Cách thứ hai tạo một Generator. Các giá trị chỉ được tạo khi cần.

Có thể hình dung:

List:

RAM
┌──────────────────────────────┐
│ 0 1 2 3 4 5 ... 999999       │
│ Toàn bộ dữ liệu nằm trong RAM │
└──────────────────────────────┘


Generator:

RAM
┌──────────────────────────────┐
│ Giá trị hiện tại              │
└──────────────────────────────┘
              ↓
        tạo giá trị tiếp theo

Vì vậy, Generator đặc biệt phù hợp với những bài toán xử lý dữ liệu lớn.


Xử lý hàng triệu dòng từ file

Giả sử chúng ta có file:

users.txt

chứa 10 triệu dòng.

Cách dễ viết nhất là:

with open("users.txt", "r", encoding="utf-8") as file:
    users = file.readlines()

for user in users:
    process(user)

Vấn đề là readlines() đọc toàn bộ file vào RAM.

Thay vào đó, hãy đọc từng dòng:

def read_users(filename):
    with open(filename, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()

Sau đó:

for user in read_users("users.txt"):
    process(user)

Bây giờ chương trình không cần giữ toàn bộ 10 triệu dòng trong RAM.

Nó sẽ hoạt động theo kiểu:

Đọc dòng 1
    ↓
Xử lý dòng 1
    ↓
Đọc dòng 2
    ↓
Xử lý dòng 2
    ↓
...
Đọc dòng 10.000.000
    ↓
Xử lý

Đây là một trong những ứng dụng thực tế quan trọng nhất của Generator.


Generator giúp tiết kiệm RAM như thế nào?

Giả sử mỗi dòng dữ liệu có kích thước khoảng 1 KB.

Một file 1 triệu dòng có thể có kích thước khoảng:

1.000.000 × 1 KB ≈ 1 GB

Nếu sử dụng:

file.readlines()

Python phải tạo cấu trúc dữ liệu chứa toàn bộ các dòng.

RAM có thể nhanh chóng trở thành vấn đề.

Trong khi đó, với:

for line in file:

Python có thể xử lý từng dòng mà không cần giữ toàn bộ file trong RAM.

Đây là lý do Generator thường được sử dụng trong:

  • Xử lý log.
  • Đọc CSV lớn.
  • Xử lý dữ liệu ETL.
  • Import dữ liệu.
  • Streaming.
  • Xử lý API.
  • Phân tích dữ liệu lớn.
  • Xử lý file hàng GB.

Generator Function với yield

Một Generator Function thường được tạo bằng yield.

Ví dụ:

def even_numbers(n):
    for i in range(n):
        if i % 2 == 0:
            yield i

Sử dụng:

for number in even_numbers(10):
    print(number)

Kết quả:

0
2
4
6
8

Điểm đặc biệt là function không tạo ra một list:

[0, 2, 4, 6, 8]

Nó lần lượt tạo:

0 → 2 → 4 → 6 → 8

khi vòng lặp yêu cầu.


yield khác return như thế nào?

Đây là điểm người mới học Python thường dễ nhầm.

Với return:

def numbers():
    return [1, 2, 3, 4, 5]

Function trả về toàn bộ list:

[1, 2, 3, 4, 5]

Với yield:

def numbers():
    yield 1
    yield 2
    yield 3
    yield 4
    yield 5

Function tạo ra một Generator.

for number in numbers():
    print(number)

Mỗi lần vòng lặp cần một giá trị, Generator cung cấp giá trị tiếp theo.


Generator Expression

Python còn cung cấp một cách viết Generator ngắn gọn hơn.

List Comprehension:

numbers = [x * 2 for x in range(1000000)]

Generator Expression:

numbers = (x * 2 for x in range(1000000))

Điểm khác biệt rất quan trọng:

[...]

tạo list.

Trong khi:

(...)

tạo Generator Expression.

Ví dụ:

numbers = (x * 2 for x in range(1000000))

for number in numbers:
    print(number)

Các giá trị được tạo lần lượt thay vì tạo cả triệu phần tử ngay từ đầu.


Generator Pipeline – Xử lý dữ liệu theo từng bước

Generator đặc biệt mạnh khi kết hợp nhiều bước xử lý.

Ví dụ chúng ta có hàng triệu số:

def numbers():
    for i in range(10000000):
        yield i

Lọc số chẵn:

def even_numbers(numbers):
    for number in numbers:
        if number % 2 == 0:
            yield number

Bình phương:

def squared(numbers):
    for number in numbers:
        yield number * number

Sau đó kết hợp:

data = numbers()
data = even_numbers(data)
data = squared(data)

for value in data:
    print(value)

Dữ liệu đi qua từng bước:

10 triệu số
     ↓
lọc số chẵn
     ↓
bình phương
     ↓
xử lý kết quả

Không cần tạo ra một list khổng lồ sau mỗi bước.

Đây được gọi là Generator Pipeline.


Generator với file CSV

Generator rất hữu ích khi xử lý CSV lớn.

Ví dụ:

import csv

def read_csv(filename):
    with open(filename, newline="", encoding="utf-8") as file:
        reader = csv.DictReader(file)

        for row in reader:
            yield row

Sau đó:

for user in read_csv("users.csv"):
    if user["age"] >= "18":
        process(user)

Chương trình có thể xử lý từng record mà không cần tải toàn bộ CSV vào RAM.

Với file vài GB, cách tiếp cận này có thể khác biệt rất lớn so với việc đọc toàn bộ dữ liệu vào một list.


Generator với API

Generator cũng có thể dùng để xử lý dữ liệu trả về từ API theo từng phần.

Ví dụ:

def get_users():
    for page in range(1, 101):
        users = fetch_users(page)

        for user in users:
            yield user

Sau đó:

for user in get_users():
    process(user)

Code phía ngoài không cần quan tâm dữ liệu được chia thành bao nhiêu page.

Nó chỉ cần:

for user in get_users():
    process(user)

Generator đóng vai trò như một luồng dữ liệu giữa nguồn dữ liệu và chương trình xử lý.


Generator có thực sự không tốn RAM?

Cần hiểu chính xác:

Generator không có nghĩa là hoàn toàn không sử dụng RAM.

Generator vẫn sử dụng một lượng RAM nhất định để lưu trạng thái của nó.

Điểm quan trọng là Generator không lưu toàn bộ dữ liệu đầu ra trong RAM.

Ví dụ:

def numbers():
    for i in range(100000000):
        yield i

Generator không tạo sẵn 100 triệu số.

Nó chỉ giữ trạng thái cần thiết để tiếp tục tạo ra số tiếp theo.

Vì vậy, cách nói chính xác hơn là:

Generator giú

Categories:

Leave Comment