Một hàm biết tạm dừng
Generator trông giống một hàm thông thường, nhưng thay vì tính toàn bộ kết quả rồi trả về, nó yield từng giá trị một, tạm dừng giữa các lần yield cho đến khi nơi đang hỏi muốn lấy giá trị tiếp theo.
Ví dụ đơn giản nhất:
Để ý yield thay vì return. Lần đầu tiên for hỏi xin một giá trị, Python chạy phần thân hàm cho đến khi gặp yield 1. Hàm tạm dừng ngay tại đó, trao 1 lại cho vòng lặp và nhớ chính xác chỗ nó đã dừng, kể cả các biến. Lần lặp tiếp theo tiếp tục từ chỗ cũ: current += 1, quay lại while, yield 2. Cứ thế cho đến khi điều kiện của vòng lặp sai, lúc đó generator đơn giản là dừng lại.
Tạm dừng rồi tiếp tục: toàn bộ bí quyết nằm ở đó.
Sao không tạo luôn một list?
Vì phiên bản dùng list phải cấp phát tất cả giá trị ngay từ đầu:
Với 5 phần tử thì ổn. Giờ hãy tưởng tượng bạn muốn 50 triệu số nguyên, và bạn chỉ quan tâm đến số đầu tiên thỏa mãn một điều kiện nào đó. Phiên bản list cấp phát 50 triệu số rồi bạn vứt đi gần hết. Phiên bản generator chỉ tạo đúng số lượng mà nơi dùng cần. Khi vòng lặp for tìm được thứ nó muốn và break, generator đơn giản là dừng lại.
Đó là điều đáng ghi nhớ: generator cho phép bạn viết code lặp mà không cần quyết định trước mình sẽ cần bao nhiêu kết quả.
Generator expression
Nếu bạn đã từng viết list comprehension, bạn đã biết cú pháp rồi: chỉ cần đổi ngoặc vuông thành ngoặc tròn:
squares_gen chưa tính gì cả. Nó chỉ là một công thức. Lặp qua nó sẽ chạy công thức từng bước một.
Generator expression rất hợp để làm đối số cho các hàm nhận một iterable:
Không có list trung gian nào. sum, max và any đọc từng giá trị một, đúng như cách chúng muốn.
Đọc một tệp lớn theo từng dòng
Đây là trường hợp thực tế kinh điển của generator: xử lý một tệp quá lớn để nạp vào bộ nhớ:
def parse_log_lines(path):
with open(path) as f:
for line in f:
if line.startswith("ERROR"):
yield line.rstrip()
for error in parse_log_lines("app.log"):
print(error)
Tệp được đọc theo kiểu lười. Mỗi lần gọi generator sẽ lấy một dòng từ ổ đĩa, lọc nó rồi yield. Lượng bộ nhớ dùng giữ nguyên bất kể kích thước tệp.
Dùng một lần là hết
Generator chỉ duyệt được một lượt. Sau khi bạn lặp đến cuối, nó bị cạn:
Vòng lặp thứ hai không in ra gì. Generator không còn gì để trả.
Nếu cần lặp nhiều lần, hoặc bạn gọi lại hàm generator để có một generator mới, hoặc chuyển dãy thành list bằng list(...) rồi lặp qua list đó bao nhiêu lần tùy ý. Chọn dựa trên chi phí: tạo lại cũng ổn nếu việc tính toán nhẹ; dùng list cũng ổn nếu dãy nhỏ.
next() và lặp thủ công
Bạn không bắt buộc phải dùng vòng lặp for. next() lấy ra từng giá trị một:
StopIteration là cách generator báo hiệu "Tôi xong rồi." Vòng lặp for bắt nó một cách âm thầm. Khi tự viết code thủ công, bạn có thể truyền giá trị mặc định cho next(gen, default) để tránh ngoại lệ này.
Generator vô hạn
Vì giá trị được tạo khi cần, một generator có thể biểu diễn một dãy không có điểm kết thúc, miễn là nơi dùng biết lúc nào thôi hỏi:
while True với một yield bên trong không làm treo chương trình. Nó chỉ có nghĩa là "nếu còn ai hỏi thì cứ tiếp tục tạo." Nơi dùng quyết định khi nào dừng.
Mẫu này xuất hiện trong xử lý dữ liệu dạng luồng, vòng lặp sự kiện và bất cứ đâu bạn lấy giá trị từ một nguồn không có độ dài xác định.
yield from: ủy quyền cho một iterable khác
Nếu generator của bạn muốn yield mọi giá trị từ một iterable khác, yield from làm việc đó trong một dòng:
Không có yield from, bạn sẽ phải viết một vòng lặp for lồng nhau với yield x bên trong. Nó cũng chuyển tiếp đúng các lời gọi send() và throw() nếu bạn có dùng đến, nhưng với code thường ngày, cứ hiểu nó là "yield mọi giá trị từ thứ này."
Khi nào nên dùng generator
Ba dấu hiệu cho thấy generator là công cụ phù hợp:
- Dãy giá trị lớn, có thể vô hạn, hoặc tốn kém nếu tạo ra toàn bộ.
- Nơi dùng có thể dừng trước khi đến cuối (ví dụ
breakngay khi tìm thấy kết quả đầu tiên). - Bạn muốn nối chuỗi các phép biến đổi như lọc, ánh xạ, lấy vài phần tử, mà không tạo các list trung gian.
Và khi không nên dùng:
- Bạn cần truy cập ngẫu nhiên (
seq[42]). Generator chỉ đi về phía trước. - Bạn cần lặp cùng một dãy nhiều lần. Hãy dùng list.
- Dãy nhỏ và bạn đã có sẵn nó. List comprehension đơn giản hơn.
Generator, list comprehension và list thông thường đều là câu trả lời đúng cho những việc khác nhau. Kỹ năng ở đây là chọn được cái phù hợp mà không cần nghĩ quá lâu, và cách nhanh nhất để có được bản năng đó là để ý, với mỗi vòng lặp bạn viết, xem "tạo hết mọi thứ trước" hay "tạo từng cái một" hợp hơn.
Tiếp theo: context manager chuyên sâu
Bạn đã thấy hầu hết các cách viết Python dùng cho việc lặp. Tiếp theo là context manager, tức câu lệnh with, và chúng kết hợp rất tốt với generator khi truyền dữ liệu dạng luồng từ tệp và kết nối mạng.
Câu hỏi thường gặp
Generator trong Python là gì?
Generator là một hàm tạo ra từng giá trị một, tạm dừng giữa các giá trị. Bạn viết nó bằng def như hàm bình thường, nhưng dùng yield thay vì return. Gọi hàm này sẽ trả về một đối tượng generator; mỗi lần lặp của for hoặc mỗi lần gọi next() sẽ chạy hàm cho đến yield kế tiếp.
List và generator khác nhau thế nào?
List giữ mọi phần tử trong bộ nhớ cùng một lúc. Generator tính từng phần tử khi cần và quên chúng đi sau khi đã dùng. Với dãy lớn hoặc vô hạn, generator chỉ tốn một lượng bộ nhớ nhỏ và cố định; với kết quả nhỏ mà bạn cần dùng lại nhiều lần, list là lựa chọn tốt hơn.
Có thể lặp qua một generator hai lần không?
Không. Generator bị cạn sau lượt duyệt đầu tiên, nên vòng lặp for thứ hai trên nó không cho ra gì. Nếu cần lặp nhiều lần, hãy gọi lại hàm generator để có một generator mới, hoặc chuyển kết quả thành một list.