Chương 06 · Iterators, Generators & Itertools

Lặp lười, sequence vô hạn và toolset itertools

Trong Python, for không lặp trên "mảng" — nó lặp trên iterator protocol. Hiểu protocol này mở cánh cửa sang generator (lazy sequence), itertools (toolset combinatoric + stream), và là nền tảng cho async for, decorator wrap generator, cho cả pytest fixture. Chương này biến hai từ khoá nhỏ — yielditer — thành siêu năng lực.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-5
🎯 Mục tiêu chương
  • Phân biệt Iterable (có __iter__) và Iterator (có __next__) — vì sao đây là 2 khái niệm khác nhau.
  • Đọc được vòng for như "đường tốc ký" cho cặp iter() + next().
  • Tự build iterable bằng class với __iter__/__next__.
  • Viết generator function bằng yield; viết generator expression bằng dấu ().
  • Hiểu lazy vs eager và khi nào nên chọn cái nào.
  • Tạo sequence vô hạn mà không treo máy — kết hợp với islice/break.
  • Dùng yield from để delegate, hiểu vì sao nó hơn vòng for ... yield.
  • Biết các tool kinh điển trong itertools: chain, count, cycle, repeat, islice, groupby, combinations, permutations, product, accumulate.
  • Nắm khái niệm send()/throw()/close() như nền cho coroutine cũ.

1. Iterable vs Iterator — hai khái niệm tách bạch

Đây là chìa khoá để hiểu mọi thứ về sau. Python tách bạch 2 khái niệm:

  • Iterable: đối tượng có thể bắt đầu lặp. Có method __iter__() trả về một iterator. Ví dụ: list, tuple, str, dict, set, range, file object.
  • Iterator: đối tượng đang trong tiến trình lặp. Có method __next__() trả về phần tử kế tiếp hoặc raise StopIteration khi hết.
Iterable
__iter__()
VD: list, str, dict, range
iter(x)
Iterator
__next__()
Trả value hoặc raise StopIteration

Mọi iterator cũng là iterable (__iter__ của iterator trả về chính nó). Nhưng iterable chưa chắc đã là iterator — list không có __next__, bạn phải gọi iter() để có iterator của nó.

🧠 Ẩn dụ — quyển sách và cái thư mục

Iterable = quyển sách. Có thể "bắt đầu đọc" nhiều lần, từ trang 1.
Iterator = cái thư mục đang kẹp trang. Mỗi lần "next" lật một trang. Đọc hết thì báo "hết" (StopIteration). Một thư mục chỉ đi được một chiều, không tua lại.

2. Built-in iter()next()

Vòng for x in xs thực ra là cú pháp đường cho đoạn này:

for loop được Python dịch thành gì
# Python viết:
for x in [10, 20, 30]:
    print(x)

# Ngầm tương đương:
_it = iter([10, 20, 30])   # lấy iterator
while True:
    try:
        x = next(_it)               # lấy giá trị kế tiếp
    except StopIteration:
        break                      # hết → thoát vòng
    print(x)

iter(obj) gọi obj.__iter__(). next(it) gọi it.__next__(). Cả hai có thể tự gọi tay khi cần điều khiển tinh vi.

Gọi tay iter/next
it = iter([1, 2, 3])
next(it)        # 1
next(it)        # 2
next(it)        # 3
next(it)        # StopIteration

# next có default — tránh exception
it = iter([1])
next(it, 'hết')   # 1
next(it, 'hết')   # 'hết'  (không raise)
🔥 Gotcha — iterator chỉ đi được một chiều
it = iter([1, 2, 3])
list(it)        # [1, 2, 3]   ← consume hết
list(it)        # []          ← rỗng! iterator đã "cạn"

# Nhưng iterable lấy lại được iterator mới:
xs = [1, 2, 3]
list(iter(xs))  # [1, 2, 3]
list(iter(xs))  # [1, 2, 3]   ← mỗi lần iter() tạo iterator MỚI

Bug điển hình: lưu generator (là iterator) vào biến, rồi for nó hai lần — lần hai sẽ "không có gì". Nếu cần lặp nhiều lần, hãy ép sang list/tuple hoặc viết hàm trả generator mới mỗi lần gọi.

3. Tự build iterable bằng class

Có 2 cách thiết kế:

  1. Class chính nó là iterator: implement cả __iter__ (trả self) lẫn __next__. Đơn giản, nhưng không lặp lại được.
  2. Tách iterable và iterator: class chính là iterable, __iter__ trả về một object iterator riêng. Hỗ trợ lặp đồng thời / nhiều lần.
Cách 1 — class kiêm cả hai
class Countdown:
    """Đếm ngược từ start về 0."""

    def __init__(self, start: int):
        self.current = start

    def __iter__(self):
        return self        # chính nó là iterator

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        v = self.current
        self.current -= 1
        return v

# Sử dụng
for n in Countdown(3):
    print(n)     # 3, 2, 1, 0

# Gotcha: chạy lại không reset
c = Countdown(3)
list(c)        # [3, 2, 1, 0]
list(c)        # []   ← state đã hết
Cách 2 — tách iterable / iterator (lặp nhiều lần)
class Range:
    def __init__(self, stop: int):
        self.stop = stop

    def __iter__(self):
        return _RangeIter(self.stop)  # iterator MỚI mỗi lần

class _RangeIter:
    def __init__(self, stop):
        self.i = 0
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.i >= self.stop:
            raise StopIteration
        v = self.i
        self.i += 1
        return v

r = Range(3)
list(r)        # [0, 1, 2]
list(r)        # [0, 1, 2]   ← lặp lại OK
💡 Mẹo — quy luật chọn

Nếu object đại diện collection (như list, range, queryset) — chọn cách 2 để lặp nhiều lần được.
Nếu object đại diện stream / cursor (như iterator file, network) — cách 1 đủ.
Trong 95% trường hợp tự build, bạn nên dùng generator function (mục 4 dưới) thay vì viết tay 2 class — Python sẽ tự lo state machine.

4. Generator function — yield

Generator function là hàm có chứa yield. Khi gọi, nó không chạy ngay mà trả về một generator object (vừa là iterable vừa là iterator). Mỗi next() chạy thân hàm đến yield tiếp theo, trả value, rồi pause giữ nguyên state local.

Generator cơ bản
def count_up_to(n: int):
    for i in range(n):
        yield i

g = count_up_to(3)   # KHÔNG chạy — chỉ tạo generator object
print(type(g))         # <class 'generator'>

next(g)               # 0   (chạy đến yield đầu, pause)
next(g)               # 1
next(g)               # 2
next(g)               # StopIteration

# Hoặc consume bằng for / list
list(count_up_to(5))   # [0, 1, 2, 3, 4]

for i in count_up_to(3):
    print(i)            # 0, 1, 2
CREATED
Vừa gọi f(), chưa chạy.
SUSPENDED
Đang pause ở yield. State được giữ.
EXECUTING
Đang chạy code giữa 2 yield.
CLOSED
Đã return hoặc raise. Mọi next() đều StopIteration.
🧠 Mental model — hàm có nút Pause

Hàm thường: gọi → chạy đến hết → return.
Generator function: gọi → không chạy gì → trả "remote control". Mỗi lần bấm next(), chạy đến nút "Pause" tiếp theo (yield), giao giá trị, rồi dừng — biến local vẫn còn nguyên. Khi return / hết thân hàm → raise StopIteration tự động.

Có thể có nhiều yield theo dòng tuyến tính:

def vai_buoc():
    print('A')
    yield 1
    print('B')
    yield 2
    print('C')

g = vai_buoc()
next(g)    # in 'A', trả 1
next(g)    # in 'B', trả 2
next(g)    # in 'C', rồi StopIteration

5. Generator expression — (x for x in ...)

Cùng cú pháp với list comprehension, nhưng đổi [] thành ():

List comp vs gen expr
# List comprehension — EAGER, tạo cả list ngay
squares_list = [x**2 for x in range(10)]
type(squares_list)        # <class 'list'>
len(squares_list)         # 10

# Generator expression — LAZY, không compute trước
squares_gen = (x**2 for x in range(10))
type(squares_gen)         # <class 'generator'>
len(squares_gen)          # TypeError — generator không có len

# Khi truyền vào hàm nhận iterable, có thể bỏ () thừa:
sum(x**2 for x in range(10))    # 285  — đẹp, gọn
max(len(w) for w in words)
💡 Ưu tiên gen expr cho pipeline lớn

Khi xử lý file 10GB, log stream, hay sequence vô hạn — luôn dùng generator expression để tránh materialize toàn bộ vào RAM. Nếu data nhỏ và cần lặp nhiều lần — list comprehension lại tốt hơn (cache result, không cần regenerate).

6. Lazy vs eager — đánh đổi RAM và CPU

List comprehension (eager)Generator (lazy)
Khi nào computeNgay khi tạoKhi next()
Bộ nhớO(n) — giữ cả collectionO(1) — chỉ giữ state hiện tại
Lặp lạiBao nhiêu lần cũng đượcMột lần, hết là cạn
Random accesslst[5]Không — phải next() tuần tự
len()Không
Vô hạn?❌ — sẽ hết RAM
Sequence rất lớn / fileTránhIdiomatic
Đo memory thực tế
import sys

# 10 triệu int
lst = [i for i in range(10_000_000)]
gen = (i for i in range(10_000_000))

sys.getsizeof(lst)      # ~80,000,056 bytes  (~80MB)
sys.getsizeof(gen)      # 200 bytes  ← chênh 400,000 lần
🔥 Gotcha — generator không có len, không indexable
gen = (x for x in range(10))
len(gen)        # TypeError: object of type 'generator' has no len()
gen[3]          # TypeError: 'generator' object is not subscriptable

# Nếu cần — ép sang list, nhưng nhớ đánh đổi RAM:
len(list(gen))  # 10   nhưng gen đã consume xong, lần sau sẽ là 0

7. Generator vô hạn

Vì generator lazy, ta có thể viết sequence vô hạn mà vẫn an toàn — miễn người consume biết dừng.

Counter vô hạn
def count():
    i = 0
    while True:
        yield i
        i += 1

# Cách 1: break khi đủ
for x in count():
    if x > 5:
        break
    print(x)        # 0, 1, 2, 3, 4, 5

# Cách 2: itertools.islice (slicing iterator)
from itertools import islice
list(islice(count(), 6))   # [0, 1, 2, 3, 4, 5]
⚠️ Đừng list() generator vô hạn

list(count()) sẽ chạy mãi và treo máy. Luôn islice hoặc kiểm tra điều kiện break trước khi materialize.

Ví dụ Fibonacci infinite:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

list(islice(fibonacci(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

8. yield from — delegate generator

Khi một generator muốn "phát lại" toàn bộ giá trị của generator (hay iterable) khác, viết for x in g: yield x dài dòng. Python 3.3+ có cú pháp gọn hơn:

yield from
# Cách dài
def chain_old(*iterables):
    for it in iterables:
        for x in it:
            yield x

# Cách gọn — yield from
def chain(*iterables):
    for it in iterables:
        yield from it

list(chain([1, 2], (3, 4), range(5, 7)))
# [1, 2, 3, 4, 5, 6]

Ngoài cú pháp gọn, yield from còn forward được send(), throw(), và giá trị return của sub-generator — đây là điều phiên bản for ... yield x không làm được. Sẽ rõ hơn ở mục 9.

Flatten tree đệ quy
def flatten(items):
    for x in items:
        if isinstance(x, list):
            yield from flatten(x)   # đệ quy gọn
        else:
            yield x

list(flatten([1, [2, [3, [4, 5]], 6]]))
# [1, 2, 3, 4, 5, 6]

9. send(), throw(), close() — generator hai chiều

Generator không chỉ phát giá trị ra — nó còn nhận giá trị vào. Đây là nền tảng của coroutine cũ (trước khi async/await chính thức xuất hiện ở Python 3.5).

send() — gửi giá trị vào yield
def echo():
    while True:
        received = yield          # yield không có giá trị bên phải
        print(f'Nhận: '{received}')

g = echo()
next(g)              # PHẢI prime — chạy đến yield đầu tiên
g.send('hello')    # in 'Nhận: hello'
g.send('world')    # in 'Nhận: world'
g.close()           # raise GeneratorExit bên trong → thoát

Cơ chế: g.send(x) resume generator, biểu thức yield đang pause sẽ trả về giá trị x. Lưu ý phải next(g) hoặc g.send(None) lần đầu để generator chạy đến yield đầu tiên (gọi là priming).

throw / close
def safe_gen():
    try:
        while True:
            x = yield
            print(x)
    except ValueError:
        print('Bắt ValueError trong generator')
    finally:
        print('Cleanup')

g = safe_gen()
next(g)
g.send(1)               # in 1
g.throw(ValueError)     # inject exception
# → 'Bắt ValueError trong generator'
# → 'Cleanup'
🧠 Hiếm dùng trong code app, nhưng quan trọng để biết

Từ Python 3.5, async/await đã thay coroutine cũ. Tuy nhiên một số library (như contextlib.contextmanager sẽ thấy ở chương 7) vẫn dùng generator hai chiều. close() đặc biệt quan trọng — Python tự gọi nó khi generator bị GC, đảm bảo block finally luôn chạy để đóng resource.

10. itertools — toolset standard library

itertools là module stdlib chứa các iterator building block. Tất cả đều lazy và có thể compose với nhau thành pipeline. Đây là bảng tham chiếu ngắn:

HàmKết quả mẫuMô tả
count(5, 2) 5, 7, 9, 11, ... Đếm vô hạn từ start, bước step
cycle([1,2,3]) 1, 2, 3, 1, 2, 3, ... Lặp vô hạn iterable
repeat('x', 3) 'x', 'x', 'x' Lặp value n lần (hoặc vô hạn nếu bỏ n)
chain([1,2], [3,4]) 1, 2, 3, 4 Nối nhiều iterable
islice(it, 5, 10) Phần tử index 5..9 Slicing cho iterator (không cần list)
takewhile(p, it) Đến khi p(x) false Lấy prefix thoả điều kiện
dropwhile(p, it) Bỏ qua prefix thoả Ngược takewhile
filterfalse(p, it) Phần tử p(x) false Ngược filter
groupby(it, key) (key, group_iter) Group adjacent phần tử cùng key
accumulate(it, op) 1, 3, 6, 10 Cumulative reduce (mặc định +)
combinations(it, r) Tổ hợp không trùng C(n, r) — không phân biệt thứ tự
permutations(it, r) Hoán vị P(n, r) — phân biệt thứ tự
product(A, B) Tích Đề-các Thay nested loop
combinations_with_replacement Tổ hợp có lặp Cho phép chọn lại phần tử
pairwise(it) (1,2), (2,3), (3,4) Cặp kề nhau (Py 3.10+)
compress(it, mask) Lọc theo mask boolean Chọn phần tử ứng với True
starmap(f, iter) f(*args) cho mỗi tuple Map mà unpack arg
tee(it, n) n iterator giống nhau "Nhân bản" iterator (cẩn thận memory)
zip_longest(A, B, fillvalue) Zip đến iterable dài nhất Khác zip built-in (dừng ở ngắn nhất)

10.1. chain — nối iterable

from itertools import chain

list(chain([1, 2], (3, 4), range(5, 7)))
# [1, 2, 3, 4, 5, 6]

# chain.from_iterable — flatten 1 cấp
list(chain.from_iterable([[1, 2], [3], [4, 5]]))
# [1, 2, 3, 4, 5]

10.2. count / cycle / repeat — vô hạn

from itertools import count, cycle, repeat, islice

# count(start, step) — như range nhưng vô hạn
list(islice(count(10, 3), 5))     # [10, 13, 16, 19, 22]

# cycle — duyệt vòng vô hạn
list(islice(cycle(['A', 'B', 'C']), 7))
# ['A', 'B', 'C', 'A', 'B', 'C', 'A']

# repeat — lặp value
list(repeat('x', 3))                  # ['x', 'x', 'x']
list(islice(repeat(0), 4))           # [0, 0, 0, 0]

# Pattern: enumerate kết hợp count với offset
for i, ch in zip(count(100), 'abc'):
    print(i, ch)    # 100 a, 101 b, 102 c

10.3. islice — slice cho iterator

Không thể gen[5:10] như list — phải dùng islice:

from itertools import islice, count

list(islice(count(), 5))           # [0, 1, 2, 3, 4]   stop=5
list(islice(count(), 5, 10))       # [5, 6, 7, 8, 9]   start=5, stop=10
list(islice(count(), 0, 10, 2))    # [0, 2, 4, 6, 8]   start, stop, step

# LƯU Ý: không hỗ trợ index âm — vì iterator không biết "cuối"

10.4. groupby — group adjacent

⚠️ Chỉ group những phần tử kề nhau cùng key

Khác SQL GROUP BY! Nếu muốn group toàn bộ, phải sort() trước theo key.

groupby — không sort trước
from itertools import groupby

data = [1, 1, 2, 2, 1, 1]
for key, group in groupby(data):
    print(key, list(group))

# 1 [1, 1]
# 2 [2, 2]
# 1 [1, 1]    ← 3 group vì các nhóm "1" tách biệt
groupby — sort trước, rồi group
students = [
    {'name': 'An',    'class': 'A'},
    {'name': 'Bình',  'class': 'B'},
    {'name': 'Cường', 'class': 'A'},
    {'name': 'Dung',   'class': 'B'},
]

# PHẢI sort theo key trước!
keyfn = lambda s: s['class']
students.sort(key=keyfn)

for cls, group in groupby(students, key=keyfn):
    print(cls, [s['name'] for s in group])

# A ['An', 'Cường']
# B ['Bình', 'Dung']
💡 Khi nào không cần sort

Khi bạn muốn group run-length cùng dạng "đường chạy giống nhau liên tiếp" — ví dụ run-length encoding, phát hiện block log liên tiếp cùng level, hay chia file theo line ngắt — groupby trên dữ liệu chưa sort lại đúng yêu cầu.

10.5. combinations / permutations / product

from itertools import combinations, permutations, product

# combinations — chọn r, KHÔNG quan trọng thứ tự
list(combinations([1, 2, 3], 2))
# [(1, 2), (1, 3), (2, 3)]

# permutations — chọn r, CÓ quan trọng thứ tự
list(permutations([1, 2, 3], 2))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

# product — tích Đề-các (thay nested loop)
list(product(['a', 'b'], [1, 2]))
# [('a', 1), ('a', 2), ('b', 1), ('b', 2)]

# product có repeat — duyệt mọi chuỗi length n
list(product([0, 1], repeat=3))
# [(0,0,0), (0,0,1), (0,1,0), ..., (1,1,1)]   ← bit pattern length 3
🧠 Phân biệt 3 hàm
  • combinations(n, r): C(n,r) — chọn nhóm. "3 trong 10 người, không quan tâm thứ tự".
  • permutations(n, r): P(n,r) — sắp xếp. "Xếp hàng 3 người từ 10".
  • product(A, B): |A| × |B| — bộ đôi từ 2 tập. "Mọi cặp (size, màu)".

10.6. accumulate — cumulative reduce

from itertools import accumulate
import operator

list(accumulate([1, 2, 3, 4]))
# [1, 3, 6, 10]    ← prefix sum (mặc định toán tử +)

list(accumulate([1, 2, 3, 4], operator.mul))
# [1, 2, 6, 24]    ← prefix product (factorial-like)

list(accumulate([5, 2, 8, 1], max))
# [5, 5, 8, 8]     ← running max

# Có thể truyền initial (Py 3.8+)
list(accumulate([1, 2, 3], initial=100))
# [100, 101, 103, 106]

10.7. Compose pipeline

Vẻ đẹp thật sự của itertools là khả năng compose: nối nhiều iterator lại như đường ống.

Pipeline streaming — không materialize trung gian
from itertools import count, islice, takewhile

# Lấy 5 số chẵn đầu > 100
result = list(islice(
    (x for x in count(100) if x % 2 == 0),
    5
))
# [100, 102, 104, 106, 108]

# takewhile — lấy đến khi false (KHÁC filter ở chỗ dừng hẳn)
result = list(takewhile(lambda x: x < 50, count(40, 2)))
# [40, 42, 44, 46, 48]

Bài tập

Bài 1 — class Countdown với __iter__/__next__

Viết class Countdown:

  • Countdown(n) tạo iterator đếm ngược từ n xuống 0.
  • Implement __iter____next__.
  • Hỗ trợ for x in Countdown(5): in 5, 4, 3, 2, 1, 0.
  • Tự reset mỗi khi gọi iter() lại (cách tách iterable / iterator).
Đáp án
class Countdown:
    def __init__(self, n: int):
        self.n = n

    def __iter__(self):
        return _CountdownIter(self.n)


class _CountdownIter:
    def __init__(self, n):
        self.current = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        v = self.current
        self.current -= 1
        return v


c = Countdown(3)
list(c)    # [3, 2, 1, 0]
list(c)    # [3, 2, 1, 0]   reset OK

Cách generator-based gọn hơn nhiều — chỉ 4 dòng:

class Countdown:
    def __init__(self, n): self.n = n
    def __iter__(self):
        for i in range(self.n, -1, -1):
            yield i

Đây là pattern điển hình — để generator tự lo state machine.

Bài 2 — Fibonacci vô hạn + 10 số đầu

Viết generator fibonacci() sinh dãy Fibonacci vô hạn. Sau đó dùng itertools.islice lấy 10 số đầu.

Test:

list(islice(fibonacci(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Đáp án
from itertools import islice

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

list(islice(fibonacci(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Vì sao không hỏng máy? Lazy. yield a pause cho đến khi islice hỏi giá trị tiếp theo. Sau 10 lần, islice không hỏi nữa — generator chỉ đơn giản nằm đó cho đến khi bị GC.

Bài 3 — Đọc file lazy bằng generator

Viết read_lines(path) trả generator các dòng đã strip() bỏ ký tự xuống dòng. Yêu cầu: không load cả file vào RAM (file có thể 10GB).

Bonus: chain với generator khác để đếm số dòng chứa từ khoá 'ERROR'.

Đáp án
def read_lines(path: str):
    with open(path, encoding='utf-8') as f:
        for line in f:
            yield line.rstrip('\n')

# Đếm dòng ERROR — pipeline streaming
err_count = sum(1 for line in read_lines('app.log')
                if 'ERROR' in line)

Cách viết thanh lịch hơn dùng yield from:

def read_lines(path):
    with open(path, encoding='utf-8') as f:
        yield from (line.rstrip('\n') for line in f)

File object Python đã iterator over lines — không cần readlines() (eager, load hết). Pattern này là idiomatic Python.

Bài 4 — Producer-consumer với generator

Viết 2 generator:

  • produce(n) sinh ra n số ngẫu nhiên từ 1-100.
  • moving_average(stream, window=3) nhận một iterator số, trả generator của trung bình cộng window phần tử gần nhất.

Test: list(moving_average(produce(7), 3)) trả 5 giá trị (đợi đủ 3 phần tử mới có output đầu tiên).

Đáp án
import random
from collections import deque

def produce(n: int):
    for _ in range(n):
        yield random.randint(1, 100)

def moving_average(stream, window: int = 3):
    buf = deque(maxlen=window)
    for x in stream:
        buf.append(x)
        if len(buf) == window:
            yield sum(buf) / window

list(moving_average(produce(7), 3))
# [v1, v2, v3, v4, v5]   — 7 input → 5 output

Đây là pattern streaming: producer và consumer pause/resume đan xen, không bao giờ tồn tại cả dataset đầy đủ trong RAM. Cùng cách Python hỗ trợ asyncio queue ở chương 8.

Bài 5 — groupby log theo ngày

Cho list log entries:

logs = [
    {'time': '2026-01-15 10:00', 'level': 'INFO',  'msg': 'a'},
    {'time': '2026-01-16 08:12', 'level': 'ERROR', 'msg': 'b'},
    {'time': '2026-01-15 14:30', 'level': 'INFO',  'msg': 'c'},
    {'time': '2026-01-16 09:00', 'level': 'WARN',  'msg': 'd'},
    {'time': '2026-01-15 09:00', 'level': 'INFO',  'msg': 'e'},
]

Group theo ngày (10 ký tự đầu của 'time') bằng itertools.groupby. Mỗi nhóm in ngày và số message.

Đáp án
from itertools import groupby

date_of = lambda log: log['time'][:10]

# BẮT BUỘC sort trước theo cùng key
logs.sort(key=date_of)

for day, entries in groupby(logs, key=date_of):
    entries = list(entries)   # materialize để dùng nhiều lần
    print(f'{day}: {len(entries)} entries')
    for e in entries:
        print(f'  [{e["level"]}] {e["msg"]}')

# 2026-01-15: 3 entries
#   [INFO] a / [INFO] c / [INFO] e
# 2026-01-16: 2 entries
#   [ERROR] b / [WARN] d

Quên sort sẽ ra nhiều group nhỏ thay vì 2 group lớn. Group bằng dict (collections.defaultdict(list)) là cách thay thế phổ biến khi không cần adjacent.

Quiz

Q1

range(10)iterable hay iterator?

Xem đáp án
✓ Đáp án

Iterable. Có __iter__ trả về một iterator mới mỗi lần gọi, nhưng không có __next__. Vì vậy r = range(10) rồi list(r) hai lần đều ra [0..9] — mỗi iter(r) tạo iterator mới.

Đối lập, g = (x for x in range(10))iterator (cũng là iterable nhưng __iter__ trả chính nó) — consume xong là cạn.

Q2

Generator object có hash được không?

Xem đáp án
✓ Đáp án

— generator là object Python, có id() và mặc định hash theo identity. Tuy nhiên hiếm khi nên dùng generator làm key dict / set vì 2 generator object khác nhau (dù cùng "định nghĩa") sẽ có hash khác — không có ý nghĩa logic.

Trong thực tế, nếu muốn dùng "sequence" làm key, hãy tuple(gen) để có giá trị immutable định danh được.

Q3

Cho gen = (x*2 for x in range(5)). So sánh:

[x for x in gen]
list(gen)

Kết quả khác hay giống?

Xem đáp án
✓ Đáp án

Giống về kết quả (đều ra [0, 2, 4, 6, 8]) — và đều consume generator. Cả hai cú pháp đều "drain" generator hết, nên nếu chạy list(gen) sau đó sẽ ra [].

Khác nhau ở style: list(gen) ngắn, idiomatic. [x for x in gen] dài hơn nhưng dễ thêm filter/map: [x*3 for x in gen if x > 2].

Q4

Generator có thể reuse (lặp lại nhiều lần) không?

Xem đáp án
✓ Đáp án

Không. Một khi consume xong, mọi next() tiếp theo đều raise StopIteration. Sai lầm phổ biến:

data = (x for x in range(5))
print(sum(data))    # 10
print(max(data))    # ValueError — sequence rỗng

Fix: ép sang list (data = list(...)), hoặc viết lại như function trả generator mới mỗi lần gọi (def make(): yield from range(5)).

Q5

So sánh:

yield from sub

for x in sub:
    yield x

Hai cái khác nhau ở chỗ nào?

Xem đáp án
✓ Đáp án

Giống về output nếu consumer chỉ next()/iter. Nhưng yield from còn forward:

  • send(value) — gửi value vào sub-generator đang pause.
  • throw(exc) — inject exception vào sub-generator.
  • return value bên trong sub — trở thành giá trị của biểu thức yield from.

Cách viết dài for x in sub: yield x không truyền được những thứ này — sub-generator không nhận được send/throw, return value bị mất. Vì vậy với coroutine cũ và compose generator, yield from là cách chuẩn.

Q6

list(groupby([1, 1, 2, 2, 1, 1])) với list() mỗi group, kết quả?

Xem đáp án
✓ Đáp án

3 group: (1, [1, 1]), (2, [2, 2]), (1, [1, 1]). groupby chỉ gộp phần tử kề nhau cùng key, không tổng hợp toàn cục như SQL.

Muốn 2 group tổng (mọi "1" gộp với nhau), phải sort trước:

data = [1, 1, 2, 2, 1, 1]
sorted_data = sorted(data)
[(k, list(g)) for k, g in groupby(sorted_data)]
# [(1, [1, 1, 1, 1]), (2, [2, 2])]

Đây là gotcha nổi tiếng nhất của itertools.groupby. Quy luật: sort trước, group sau.

Q7

Generator nhanh hơn list, đúng hay sai?

Xem đáp án
✓ Đáp án

Phụ thuộc tình huống.

  • Sinh 1 lần, hoặc chỉ cần phần đầu (any, next, break sớm): generator nhanh hơn — không tốn thời gian build cả list, không tốn RAM.
  • Đọc nhiều lần / random access / cần length: list nhanh hơn — đã cache sẵn, không phải tính lại. Generator phải regenerate (hoặc cạn).
  • Sequence rất lớn / vô hạn / streaming: generator là lựa chọn duy nhất — list không khả thi.

Quy tắc thực tế: start with list comp; switch to gen expr khi gặp vấn đề memory hoặc khi pipe vào hàm chỉ duyệt một lần (sum, max, any, "".join).

Tổng kết

Sau chương 6, bạn nên đã master:

  • Iterable vs Iterator: 2 protocol tách bạch — __iter__ mở phiên, __next__ bước.
  • iter() / next(): built-in để điều khiển tay; for là syntactic sugar.
  • Custom iterable bằng class: 2 pattern — class kiêm iterator (1 lần) vs tách iterable / iterator (lặp lại).
  • Generator function: yield — pause/resume, state machine tự động.
  • Generator expression: () thay []; lazy, O(1) memory.
  • Lazy vs eager: chọn theo memory/CPU/lặp lại/length/random access.
  • Sequence vô hạn: while True: yield + islice/break.
  • yield from: delegate, forward send/throw/return.
  • send / throw / close: generator 2 chiều — nền tảng coroutine cũ.
  • itertools: chain, count, cycle, repeat, islice, groupby (adjacent!), combinations, permutations, product, accumulate, takewhile, pairwise.

Kết nối

  • Chương 7 (Decorators & Context Managers)@contextmanager dùng generator để biến function thành context manager; nhiều decorator pattern (cache, retry) wrap generator function.
  • Chương 8 (Asyncio & Async)async def + yield = async generator; async for mở rộng iterator protocol cho coroutine.
  • Chương 10 (pytest & Testing) — pytest fixture với yield chính là generator: code trước yield = setup, code sau = teardown.
  • JavaScript Chương 5 — đối chiếu function*/yield của JS; iterator protocol JS dùng { value, done } object thay vì StopIteration exception như Python.
  • Dart Chương 6sync*/yield Dart; Iterable Dart cũng lazy như generator Python.