🍽️ Khám Phá Iterators Trong Python
Trong phần 1, chúng ta đã làm quen với buffet (iterables) - nguồn thực phẩm vô tận. Nhưng buffet không tự phục vụ được. Bạn cần một người phục vụ đi dọc theo hàng, nhớ nơi bạn đã dừng lại, và đưa cho bạn món ăn tiếp theo.
Đó chính là iterator:
👉 Một người phục vụ với vé một chiều không thể quay lại.
📋 Điều Bạn Sẽ Học Được
Cuối bài viết này, bạn sẽ biết:
- Thế nào là một iterator (
__iter__
và__next__
). - Cách
StopIteration
thực sự kết thúc một vòng lặpfor
. - Cách CPython đại diện cho các iterator trong bộ nhớ (nhẹ, dựa trên con trỏ).
- Tại sao generators chỉ là những người phục vụ tinh vi nhờ vào
yield
. - Những điều thú vị, cạm bẫy và mẹo gỡ lỗi.
Hãy lấy một đĩa, chúng ta cùng bắt đầu.
🍴 Mở đầu Cuộc Sống Của Người Phục Vụ
Hãy tưởng tượng bạn đang ở buffet:
- Buffet (iterable) nói: “Đây là một người phục vụ.”
- Người phục vụ (iterator) nói: “Để tôi phục vụ bạn món đầu tiên.”
- Mỗi khi bạn gọi
next()
, người phục vụ bước thêm một bước. - Khi không còn thực phẩm? Người phục vụ bỏ khay và hét lên: StopIteration!
Điểm Chú Ý:
👉 Người phục vụ là có trạng thái. Anh ta nhớ nơi anh đã dừng lại.
⚙️ Iterator Là Gì? (Hợp Đồng)
Python định nghĩa một iterator với 2 quy tắc đơn giản:
__iter__() # phải trả về self
__next__() # phải trả về món tiếp theo, hoặc raise StopIteration
Ví Dụ:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
val = self.current
self.current -= 1
return val
c = Countdown(3)
print(list(c)) # [3, 2, 1]
👉 Lưu ý điều kỳ lạ: __iter__()
trả về self
.
Bởi vì người phục vụ vừa là iterable vừa là iterator.
Đó là lý do tại sao các iterator là một lần - khi đã hết, chúng không còn sử dụng được nữa.
🧠 Cấu Trúc Nội Tại Của Các Đối Tượng Iterator Trong CPython
Trong CPython (phiên bản tham chiếu của Python):
- Một đối tượng
list_iterator
có:ob_ref
: con trỏ đến danh sách gốc.index
: một con trỏ số nguyên (bắt đầu từ 0).
Mỗi lần gọi next()
sẽ:
- Nhìn vào
list[index]
. - Tăng chỉ số lên.
- Trả về đối tượng.
- Nếu
index >= len(list)
, raiseStopIteration
.
Chi Phí Bộ Nhớ:
- Iterator chỉ là một cấu trúc nhỏ (con trỏ + chỉ số).
- Nó không sao chép danh sách.
Đó là lý do tại sao các iterator rất nhẹ - chỉ vài byte thay vì sao chép dữ liệu của bạn.
🛑 StopIteration - “Xin lỗi, không còn thực phẩm”
Khi một iterator kết thúc, nó raise StopIteration.
Nhưng bạn hầu như không bao giờ thấy nó vì các vòng lặp for
và comprehension xử lý nó một cách êm ái.
Ví Dụ:
it = iter([1, 2])
while True:
try:
item = next(it)
except StopIteration:
break
print(item)
Đó là những gì for x in [1,2]:
biên dịch thành.
StopIteration
là tín hiệu để phá vỡ vòng lặp.
🔬 Phân Tích: Iterables So Với Iterators
Hãy tóm tắt với các phép ẩn dụ về buffet:
Đối Tượng | Ai Là Ai Trong Nhà Hàng? | Giao Thức | Nhiều Lần? |
---|---|---|---|
Iterable | Bàn buffet | __iter__ |
Có (người phục vụ mới mỗi lần) |
Iterator | Người phục vụ với đĩa | __iter__ + __next__ |
Không (một chuyến duy nhất) |
⚡ Generators: Người Phục Vụ Tự Động
Việc viết __next__
bằng tay có vẻ hơi khó khăn. Đó là lý do tại sao Python cho chúng ta generators.
Một generator chỉ là một hàm đặc biệt với yield
ghi nhớ trạng thái của nó giữa các lần gọi.
def countdown(n):
while n > 0:
yield n # tạm dừng tại đây
n -= 1
c = countdown(3)
print(next(c)) # 3
print(next(c)) # 2
print(next(c)) # 1
print(next(c)) # StopIteration
Cách Hoạt Động:
- Khi bạn gọi
countdown(3)
, Python tạo ra một đối tượnggenerator
. - Đối tượng đó có một khung ngăn xếp và một con trỏ hướng dẫn.
- Mỗi lần gọi
next()
sẽ tiếp tục thực hiện cho đến khi gặpyield
tiếp theo.
Nó giống như một người phục vụ với một điểm lưu trong thời gian.
🔍 Thực Tế Thú Vị: Iterators Có Mặt Khắp Nơi
- Tệp →
for line in open('file.txt'):
là một iterator qua các dòng. - Từ điển →
for key in d:
lặp qua các khóa một cách lười biếng. range
→ trả về mộtrange_iterator
, không tạo danh sách khổng lồ trong bộ nhớ.zip
,map
,filter
→ tất cả đều trả về iterators.- itertools → nhà máy sản xuất iterators vô hạn hoặc lười biếng.
Về cơ bản: bất cứ khi nào Python có thể tránh việc tạo ra một danh sách khổng lồ, nó sử dụng iterator.
💾 So Sánh Bộ Nhớ: danh sách, iterator và generator
import sys
nums = [i for i in range(1_000_000)]
print(sys.getsizeof(nums)) # ~8 MB
gen = (i for i in range(1_000_000))
print(sys.getsizeof(gen)) # ~112 bytes 🤯
👉 Một danh sách giữ tất cả 1 triệu tham chiếu trong bộ nhớ.
👉 Một generator chỉ lưu trữ một khung đối tượng nhỏ.
Đó là sức mạnh của sự lười biếng.
🚨 Những Cạm Bẫy Thường Gặp
- Sự cạn kiệt của iterator
it = iter([1,2,3])
print(list(it)) # [1,2,3]
print(list(it)) # [] (đã hết!)
- Chia sẻ một iterator qua các hàm
def f(it): return list(it)
def g(it): return list(it)
it = iter([1,2,3])
print(f(it)) # [1,2,3]
print(g(it)) # [] (oops)
- Vòng lặp vô hạn
import itertools
for x in itertools.count():
print(x) # sẽ không bao giờ dừng mà không có break
🎨 Mô Hình Tư Duy ASCII
[Iterator (người phục vụ)]
|
| next()
V
item → item → item → StopIteration
Người phục vụ đi về phía trước, bỏ đĩa. Khi không còn gì trong tay, trò chơi kết thúc.
🧭 Khi Nào Sử Dụng Iterators
- ✅ Phát Streaming dữ liệu lớn (logs, CSVs, truy vấn DB).
- ✅ Kết hợp lười biếng các pipeline (
map
,filter
,itertools
). - ✅ Dãy số vô hạn với điều kiện dừng được kiểm soát.
- ❌ Không nên sử dụng nếu bạn cần truy cập ngẫu nhiên hoặc nhiều lần (sử dụng danh sách/tuple).
🎬 Kết Luận
Chúng ta đã học được:
- Iterators là những người phục vụ có trạng thái: một chuyến đi, không quay lại.
- Giao thức =
__iter__()
(trả về self) +__next__()
(trả về món hoặc StopIteration). - Các iterator trong CPython là các đối tượng nhỏ, dựa trên con trỏ.
- Generators chỉ là đường dẫn cú pháp để tạo ra các iterator.
- Iterators tiết kiệm rất nhiều bộ nhớ nhưng có thể làm bạn gặp rắc rối với sự cạn kiệt.
👉 Phần tiếp theo (Phần 3): Mẹo Lặp Nâng Cao
Chúng ta sẽ khám phá itertools
, yield from
, phân quyền generator, phân nhánh một iterator, và cách xây dựng các pipeline lười biếng tùy chỉnh như một đầu bếp chuyên nghiệp thiết kế thực đơn.