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.
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.
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.
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.
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:
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.
Đâ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.
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 đặ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 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 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ý.
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ú