← Lập trình Python nâng cao

Bài 3 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Iterator & generator

Biên soạn bởi Nguyễn Anh Tuấn

Hiểu bản chất vòng for trong Python: giao thức iterator (__iter__/__next__), generator & yield, lazy evaluation và đường ống itertools.

Vòng for trông đơn giản, nhưng bên dưới là một giao thức: Python gọi iter() để lấy một iterator, rồi gọi next() liên tục tới khi gặp StopIteration. Bấm qua từng bước:

1 def squares(n):
2 for i in range(n):
3 yield i * i
Trạng thái (giữ lại khi tạm dừng)
(chưa có)
Đã sinh (lười, lần lượt)
(chưa sinh giá trị nào)
bước 1/8
g = squares(3) - KHÔNG chạy thân hàm, chỉ tạo một generator. Đây là "lười" (lazy).
  • for x in ds ≡ it = iter(ds); rồi lặp x = next(it) tới khi StopIteration.
  • Bạn không thấy StopIteration vì for tự bắt nó để dừng.
  • Hiểu giao thức này = bạn TỰ tạo được thứ “for được”.

Iterable = lặp được (có __iter__); iterator = giữ vị trí (có __next__):

iterator.py

class DemNguoc:
    def __init__(self, n):
        self.n = n
    def __iter__(self):          # tra ve mot iterator (chinh no)
        return self
    def __next__(self):          # tra phan tu ke tiep
        if self.n <= 0:
            raise StopIteration  # het -> bao dung
        self.n -= 1
        return self.n + 1

for x in DemNguoc(3):
    print(x)

Kết quả khi chạy

3
2
1
  • __iter__ trả về iterator; __next__ trả phần tử kế, hết thì raise StopIteration.
  • Một iterator dùng XONG là cạn - phải tạo mới để lặp lại.
  • Cách này dài dòng → generator (Bước 3) làm gọn hơn nhiều.

Hàm có yield là một generator. Gọi nó KHÔNG chạy thân hàm; mỗi next() chạy tới yield kế rồi tạm dừng, giữ nguyên trạng thái:

generator.py - tương đương DemNguoc, gọn hơn nhiều

def dem_nguoc(n):
    while n > 0:
        yield n          # tra ra n roi TAM DUNG tai day
        n -= 1

for x in dem_nguoc(3):
    print(x)

Kết quả khi chạy

3
2
1
  • Gọi dem_nguoc(3) chỉ TẠO generator - chưa chạy gì (lười).
  • yield = trả một giá trị rồi tạm dừng; next() sau chạy tiếp từ chỗ đó.
  • Generator tự lo __iter__/__next__/StopIteration cho bạn.

Vì generator chỉ sinh từng phần tử khi cần, nó xử lý được dữ liệu lớn hay vô hạn mà không dựng tất cả trong RAM:

lazy.py - generator expression (không ngoặc vuông)

# List comprehension: DUNG ca 1 trieu phan tu trong bo nho
tong1 = sum([x * x for x in range(1_000_000)])

# Generator expression: LUOI, chi giu mot phan tu tai mot thoi diem
tong2 = sum(x * x for x in range(1_000_000))

print(tong1 == tong2)

Kết quả khi chạy

True
  • [ ... ] = list (eager, tốn bộ nhớ); ( ... ) = generator (lazy).
  • Đưa thẳng vào hàm: sum(x*x for x in ds) - khỏi ngoặc trong, không dựng list trung gian.
  • Hợp đọc file lớn, luồng dữ liệu, dãy vô hạn - giữ ít trong RAM.

Generator dùng một lần

Generator cạn sau khi duyệt: lặp lần hai sẽ rỗng. Cần duyệt nhiều lần thì list(gen) để chốt hẳn thành list (materialize), hoặc tạo generator mới mỗi lần.

Generator không chỉ cho ra giá trị - nó còn nhận vào. send(v) chạy tiếp và biến v thành kết quả của yield đang dừng (đây là nền của coroutine):

send.py - generator tính trung bình động

import inspect

def averager():
    total = 0; n = 0; avg = None
    while True:
        x = yield avg        # GUI x vao day; tra avg ra ngoai
        total += x; n += 1
        avg = total / n

g = averager()
print(inspect.getgeneratorstate(g))   # GEN_CREATED
next(g)                                # "moi" toi yield dau tien
print(g.send(10))                      # 10.0
print(g.send(20))                      # 15.0  ((10+20)/2)
g.close()
print(inspect.getgeneratorstate(g))   # GEN_CLOSED

Kết quả khi chạy

GEN_CREATED
10.0
15.0
GEN_CLOSED

yield_from.py - ghép nhiều generator

def chain(*its):
    for it in its:
        yield from it        # chuyen tiep moi gia tri cua it ra ngoai

print(list(chain([1, 2], [3, 4])))

Kết quả khi chạy

[1, 2, 3, 4]
  • send(v): vừa lấy ra vừa GỬI v vào (v = kết quả của yield). Phải next() “mồi” trước.
  • close() dừng generator (trạng thái GEN_CLOSED); inspect.getgeneratorstate xem trạng thái.
  • yield from delegate sang generator/iterable con - gọn & chuyển tiếp cả send.

Generator là tiền thân của async

Khả năng “tạm dừng giữ trạng thái + nhận giá trị vào” của generator chính là nền tảng cho coroutineasync/await (bài gần cuối khoá) - nơi event loop “gửi” kết quả I/O vào các tác vụ đang chờ.

Nhiều generator nối nhau thành một đường ống xử lý lười; module itertools cho sẵn nhiều mảnh ghép:

pipeline.py

import itertools

def doc_so(ds):
    for s in ds:
        yield int(s)

def chi_chan(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

duong_ong = chi_chan(doc_so(["1", "2", "3", "4", "5", "6"]))
print(list(itertools.islice(duong_ong, 2)))   # chi lay 2 phan tu dau

Kết quả khi chạy

[2, 4]
  • Mỗi tầng generator chỉ lấy phần tử khi tầng sau yêu cầu - lười suốt đường ống.
  • itertools: count (đếm vô hạn), islice (cắt), chain (nối), takewhile/groupby… đều trả iterator.
  • Đường ống generator = xử lý dữ liệu lớn theo dòng, ít bộ nhớ.

Tiếp theo

Generator dựa trên hàm nhớ được trạng thái. Bài kế tiếp khai thác cùng ý tưởng “hàm là hạng nhất”: closure (hàm nhớ biến bao quanh) và decorator (hàm bọc hàm).

Câu hỏi thường gặp

ITERABLE là thứ LẶP ĐƯỢC (list, tuple, str, dict, file…): nó có __iter__ trả về một iterator. ITERATOR là thứ GIỮ VỊ TRÍ: nó có __next__ trả phần tử kế tiếp, hết thì ném StopIteration. Vòng for gọi iter(iterable) một lần để lấy iterator, rồi gọi next() liên tục.

for x in ds: ... tương đương: it = iter(ds); rồi lặp x = next(it) cho tới khi next() ném StopIteration thì dừng. Bạn không bao giờ thấy StopIterationfor tự bắt nó.

Hàm có yield là một GENERATOR. Gọi nó KHÔNG chạy thân hàm - chỉ trả về một generator object. Mỗi lần next(), thân chạy tới yield kế tiếp rồi TẠM DỪNG (giữ nguyên biến cục bộ); lần next() sau chạy tiếp từ chỗ dừng.

next(g) chạy tiếp và LẤY giá trị yield ra. send(v) làm điều đó NHƯNG còn GỬI v VÀO: v trở thành KẾT QUẢ của biểu thức yield đang tạm dừng (x = yield ...). Nhờ vậy generator thành hai chiều - vừa cho ra, vừa nhận vào (nền của coroutine). Phải next(g) một lần để "mồi" tới yield đầu trước khi send.

yield from iterable “chuyển tiếp” mọi giá trị của iterable đó ra ngoài, khỏi viết vòng for thủ công. Hữu ích khi GHÉP nhiều generator (delegation): def chain(*its): for it in its: yield from it. Nó cũng chuyển tiếp cả send/throw tới generator con.

Dùng LIST khi cần giữ lại để duyệt nhiều lần, cần len(), cần truy cập theo chỉ số. Dùng GENERATOR khi chỉ duyệt MỘT lần, dữ liệu lớn/vô hạn, hoặc làm “đường ống” xử lý từng phần. Lưu ý: generator dùng xong là CẠN - duyệt lần hai sẽ rỗng.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vòng for x in ds tương đương với chuỗi thao tác nào ở bên dưới?

  1. 1

    Bản chất vòng for

    Viết lại for x in [10,20,30] bằng iter()/next() thủ công, bắt StopIteration để dừng.

    Hoàn thành khi: Dùng it = iter(...)while True: next(...) trong try/except StopIteration - in 10, 20, 30 rồi dừng.

  2. 2

    Iterator rồi generator

    Viết DemNguoc(n) hai cách: (a) class với __iter__/__next__; (b) generator với yield. So số dòng.

    Hoàn thành khi: Cả hai cho 3,2,1; bản generator ngắn hơn hẳn và tự lo __iter__/__next__/StopIteration.

  3. 3

    Lười tiết kiệm bộ nhớ

    So sum([x*x for x in range(1_000_000)]) với sum(x*x for x in range(1_000_000)). Khác gì về bộ nhớ?

    Hoàn thành khi: Bản generator (không ngoặc vuông) không dựng list 1 triệu phần tử → tốn ít bộ nhớ hơn nhiều, kết quả như nhau.

  4. 4

    Generator nhận giá trị

    Viết generator averager(): x = yield avg; cập nhật trung bình. Mồi bằng next(), rồi send(10), send(20).

    Hoàn thành khi: send(10)10.0; send(20)15.0. Bạn giải thích vì sao phải next() “mồi” trước khi send.

  5. 5

    yield from

    Viết chain(*its) dùng yield from để nối nhiều iterable. Kiểm list(chain([1,2],[3,4])).

    Hoàn thành khi: [1, 2, 3, 4]; bạn thấy yield from gọn hơn vòng for lồng.