Câu 1Đáp án sát nhất với print(type(gen)) ở đây là gì?def f():
yield 1
yield 2
gen = f()
print(type(gen))
Hàm Generator với yield — Tạo giá trị từng cái một để tiết kiệm bộ nhớ
Học hàm generator trong Python với yield để sinh giá trị từng cái một, tiết kiệm bộ nhớ khi xử lý chuỗi dữ liệu lớn.
Trong bài trước bạn đã thấy closure — cách niêm phong trạng thái bên trong một hàm và trả về giá trị khác mỗi lần gọi. Python có một cơ chế chuyên dụng cho loại hàm "trả về giá trị tiếp theo mỗi lần gọi" này: hàm generator. Dùng yield thay cho return, hàm tạm dừng giữa chừng để trả về một giá trị, rồi tiếp tục từ chỗ nó dừng ở lần gọi tiếp theo.
Đây là lựa chọn tuyệt vời khi bạn không muốn vật chất hóa toàn bộ một tập hợp lớn cùng lúc và muốn stream từng giá trị một — xử lý log, crunch dữ liệu lượng lớn, và các tải tương tự.
Cơ bản về yield — Trả về một giá trị mỗi lần
Một hàm có yield value trong thân chính là một hàm generator. Gọi nó như hàm thông thường không chạy thân hàm — bạn chỉ nhận về một đối tượng generator đặc biệt.
Để thực sự lấy ra một giá trị, gọi next(obj).
next() đầu tiên chạy thân hàm đến yield đầu tiên và trả về giá trị đó.
next() tiếp theo tiếp tục từ đó và chạy đến yield kế tiếp.
Khi không còn yield nào nữa, một next() khác phát sinh StopIteration.
def simple():
yield 1
yield 2
gen = simple()
print(type(gen)) # <class 'generator'>
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) ← StopIteration (không còn yield)
Khác biệt với return
Hàm thông thường return và toàn bộ phạm vi biến mất — lần gọi tiếp theo bắt đầu từ đầu. Hàm generator thì ngược lại, tạm dừng tại yield và giữ lại biến local cùng vị trí. next() tiếp theo tiếp tục đúng chỗ bạn dừng, đó là khác biệt lớn.
Lấy giá trị bằng for — Không cần lo về StopIteration
Viết next() mỗi lần thật phiền, và bạn cũng không nên phải tự xử lý StopIteration. Thay vào đó dùng for value in generator:, và Python gọi next() ngầm bên dưới và tự thoát vòng lặp khi generator kết thúc. Đây là dạng phổ biến hơn rất nhiều.
Nếu rải vài dòng print("đang chạy...") ở cả hai phía, bạn có thể quan sát mỗi yield chuyển đổi thực thi qua lại — phía generator → phía bên gọi → phía generator.
def count_up_to(max_value):
print("bắt đầu generator")
for i in range(max_value):
print(f" trước yield: {i}")
yield i
print(f" sau yield: {i}")
for v in count_up_to(3):
print(f"nhận được: {v}")
# Luồng output:
# bắt đầu generator
# trước yield: 0
# nhận được: 0
# sau yield: 0
# trước yield: 1
# ... (tiếp tục)
Khác biệt với list — Cắt giảm bộ nhớ
Khi bạn làm việc với các giá trị từ 0 đến 999_999, một list comprehension vật chất hóa cả 1.000.000 số nguyên cùng lúc trong bộ nhớ. Generator thì ngược lại, chỉ giữ giá trị hiện tại và tính cái tiếp theo theo yêu cầu. List có dung lượng vài MB; chính đối tượng generator chỉ là vài trăm byte.
import sys
MAX = 10 ** 6
# List: load tất cả vào bộ nhớ cùng lúc
data_list = [i for i in range(MAX)]
print(sys.getsizeof(data_list))
# vd. 8000056 (~8 MB)
# Generator expression: chỉ phần tử hiện tại
data_gen = (i for i in range(MAX))
print(sys.getsizeof(data_gen))
# vd. ~200 byte
# Code phía bên gọi giống nhau
for v in data_gen:
if v > 2:
break
print(v)
# 0
# 1
# 2
Lối tắt Generator Expression
Đổi ngoặc vuông [ ... ] của list comprehension thành ngoặc tròn ( ... ) và bạn có generator expression. (i for i in range(1_000_000)) cho cùng hiệu ứng như hàm generator dựa trên def chỉ trong một dòng. Truyền thẳng vào sum() / max() / any() và bạn bè — chạy ngon.
Nối chuỗi Generator với yield from
Khi muốn một generator chuyển tiếp giá trị từ generator khác nguyên xi, bạn có thể viết yield from sub_generator trong một dòng thay vì for v in sub: yield v. Tiện khi muốn gộp nhiều nguồn dữ liệu vào một generator duy nhất.
Ví dụ, với một hàm stream doanh số chi nhánh Tokyo và một hàm khác cho Osaka, xếp yield from tokyo_sales() và yield from osaka_sales() cho phía bên gọi một generator trông như một dòng liên tục.
def tokyo_sales():
yield 1200
yield 980
def osaka_sales():
yield 850
yield 1340
def all_sales():
yield from tokyo_sales()
yield from osaka_sales()
for amount in all_sales():
print(amount)
# 1200
# 980
# 850
# 1340
yield from sub_gen() là dạng tắt của for v in sub_gen(): yield v.
Giá trị mà sub yield đi thẳng tới bên gọi bên ngoài, nên
1200, 980 của tokyo_sales,
rồi 850, 1340 của osaka_sales
đến for amount in all_sales(): theo thứ tự.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Điều gì xảy ra khi gọi next() trên một generator sau khi mọi yield đã được dùng hết?
Câu 3Hai dòng nào dùng bộ nhớ ít hơn rất nhiều?
A: data = [i for i in range(10**6)]
B: data = (i for i in range(10**6))