# Generator trong Python: yield, iterator và list, dict comprehension

URL: https://skillaria.top/vi/guides/python/generator-python-yield

Generator trong Python là đối tượng trả về các giá trị từng cái một khi được yêu cầu, không giữ toàn bộ dãy trong bộ nhớ; tạo bằng hàm có từ khóa yield hoặc biểu thức generator trong dấu ngoặc tròn. List, dict và set comprehension là cách viết ngắn để tạo một tập hợp hoàn chỉnh: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.

Generator trong Python là một đối tượng trả về các giá trị từng cái một, chỉ khi được yêu cầu, và không giữ cả dãy trong bộ nhớ. Có hai cách tạo: viết hàm với từ khóa `yield`, hoặc dùng biểu thức generator trong dấu ngoặc tròn. Bên cạnh đó là list, dict và set comprehension — cú pháp ngắn để xây dựng một tập hợp hoàn chỉnh trong một dòng. Dưới đây là cách python yield hoạt động, generator khác iterator ở điểm nào, và làm sao để comprehension không biến thành một chuỗi khó đọc.

## Iterator: nền tảng của vòng lặp for

Để hiểu generator, trước hết cần nắm iterator. Iterable là thứ có thể duyệt bằng vòng lặp: list, chuỗi, dict, tệp. Iterator là đối tượng nhớ vị trí hiện tại và mỗi lần gọi `next()` trả về phần tử tiếp theo; khi hết phần tử, nó ném ra `StopIteration`.

```
nums = [10, 20, 30]
it = iter(nums)      # lấy iterator từ list
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration
```

Vòng lặp `for` thực chất làm đúng việc này: gọi `iter()`, rồi gọi `next()` cho đến khi gặp `StopIteration`. Có thể tự viết iterator bằng một class với các phương thức `__iter__` và `__next__`:

```
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current  0:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          #
print(next(gen))    # 3
print(list(gen))    # [2, 1] — tiếp tục từ chỗ đã dừng
```

Khi hàm chạy đến cuối (hoặc gặp `return`), generator tự ném `StopIteration`, và vòng lặp `for` kết thúc êm đẹp. Đây là điểm khác biệt chính giữa yield và return: return kết thúc hàm và trả về một giá trị, còn yield tạm dừng hàm và có thể trả về bao nhiêu giá trị tùy ý.

### Generator và iterator: khác nhau ở đâu

Mọi generator đều là iterator: chúng có `__iter__` và `__next__`. Nhưng không phải iterator nào cũng là generator. Sự khác biệt nằm ở cách tạo và khả năng.

|  | Iterator (class) | Generator |
| --- | --- | --- |

| Cách tạo | class có `__iter__` và `__next__` | hàm có `yield` hoặc `(x for x in ...)` |

| Trạng thái | tự quản lý trong thuộc tính | được lưu tự động trong biến cục bộ |

| StopIteration | bạn phải tự ném | tự xuất hiện khi hàm kết thúc |

| Thêm | tùy bạn viết | `send()`, `throw()`, `close()` |

Hai loại có chung một đặc điểm quan trọng: chỉ có thể duyệt một lần. Lặp `for` lần hai trên một generator đã cạn sẽ không cho ra gì — cần tạo generator mới.

### Generator vô hạn và đọc tệp lớn

Generator không bắt buộc phải kết thúc. Dãy vô hạn vẫn an toàn miễn là bạn chỉ lấy một số lượng phần tử hữu hạn:

```
from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']
```

Ứng dụng thực tế nhất là xử lý tệp lớn theo từng dòng. Có thể nối các generator thành một pipeline, và tại mỗi thời điểm bộ nhớ chỉ chứa một dòng, chứ không phải toàn bộ tệp:

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

def only_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

for line in only_errors(read_lines("app.log")):
    print(line)
```

### yield from và send

`yield from` chuyển tiếp tất cả giá trị của một iterable khác, khỏi cần vòng lặp lồng nhau:

```
def flatten(groups):
    for g in groups:
        yield from g

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

Phương thức `send()` đưa một giá trị vào trong generator: giá trị đó trở thành kết quả của biểu thức `yield`. Trước lần `send` đầu tiên, generator phải được «khởi động» bằng `next()`. Ví dụ, đây là bộ tính trung bình chạy liên tục:

```
def averager():
    total, count = 0, 0
    avg = None
    while True:
        value = yield avg
        total += value
        count += 1
        avg = total / count

a = averager()
next(a)            # chạy đến yield đầu tiên
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0
```

Trong code ứng dụng, `send` ít khi được dùng, nhưng nó hay được hỏi trong các buổi phỏng vấn.

### range có phải là generator không?

Không. `range` là dãy lười: nó cũng không lưu hết các số, nhưng hỗ trợ lập chỉ mục, `len()`, kiểm tra `in`, và có thể duyệt bao nhiêu lần cũng được. Generator không làm được những việc đó.

```
r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # cả hai lần đều [0, 1, 2, 3, 4]
```

## List comprehension: tạo list trong một dòng

List comprehension có dạng `[biểu_thức for x in tập_hợp]`. Nó tạo một list mới và không thay đổi list gốc. Đọc từ trái sang phải: trước tiên là «đặt gì vào», sau đó là «lấy từ đâu».

```
amounts = [100, 250, 400]

# vòng lặp thông thường
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# cùng việc đó qua comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]
```

Dù người ta hay gọi là «generator list», kết quả vẫn là một list hoàn chỉnh, không phải generator: mọi phần tử được tính ngay lập tức.

### List comprehension với điều kiện if và if else

Đây là chỗ hay nhầm lẫn nhất. Điều kiện lọc đặt ở cuối và loại bỏ phần tử. Điều kiện chọn (toán tử ba ngôi) đặt ở đầu và biến mỗi phần tử thành một trong hai giá trị, không loại bỏ gì.

```
amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["lớn" if a > 200 else "nhỏ" for a in amounts]
# ['nhỏ', 'lớn', 'lớn']

# lọc và chọn cùng lúc
odd_marks = ["chẵn" if n % 2 == 0 else "lẻ" for n in range(6) if n > 2]
# ['lẻ', 'chẵn', 'lẻ']
```

Viết `[a for a in amounts if a > 200 else 0]` là lỗi cú pháp: `else` chỉ đi với điều kiện đặt ở đầu.

### Comprehension lồng nhau và list hai chiều

Hai vòng `for` được viết theo đúng thứ tự của vòng lặp lồng nhau, từ trên xuống dưới. Điều này giúp không nhầm thứ tự khi làm phẳng list của list:

```
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# tương đương:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]
```

Ma trận hai chiều được tạo bằng comprehension bên trong comprehension. Lỗi kinh điển là nhân list: các hàng sẽ là cùng một đối tượng.

```
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — cả hai hàng đều đổi

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# chuyển vị
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]
```

## Dict và set comprehension

Cùng kỹ thuật này áp dụng cho các tập hợp khác. Dấu ngoặc nhọn có dấu hai chấm tạo dict (dict comprehension), còn không có dấu hai chấm tạo set (set comprehension).

```
spent = [("ăn uống", 500), ("taxi", 300), ("ăn uống", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'ăn uống', 'taxi'} — set không đảm bảo thứ tự

last = {cat: amount for cat, amount in spent}
print(last)        # {'ăn uống': 200, 'taxi': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
```

Chú ý ở `last`: khi khóa lặp lại, giá trị cuối cùng thắng, tổng 500 đã bị mất. Nếu cần cộng dồn thì comprehension không phù hợp — hãy viết vòng lặp, hoặc dùng `collections.Counter` / `defaultdict`.

### Còn comprehension cho tuple thì sao?

Trong Python không có «tuple comprehension» riêng. Dấu ngoặc tròn tạo ra biểu thức generator, còn tuple thu được khi truyền nó vào `tuple()`:

```
t = tuple(n * 2 for n in range(3))  # (0, 2, 4)
```

## Biểu thức generator: tính khi cần thay vì lưu sẵn

Nếu thay dấu ngoặc vuông bằng dấu ngoặc tròn, bạn có biểu thức generator. Nó không tính gì trước và trả về giá trị từng cái một. Với một triệu phần tử, khác biệt về bộ nhớ là rất lớn:

```
import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # khoảng 8 MB
print(sys.getsizeof(as_gen))   # vài trăm byte
```

Biểu thức generator thường nằm bên trong các hàm tổng hợp. Nếu generator là tham số duy nhất, có thể bỏ cặp ngoặc thừa. `any()` và `all()` còn dừng ngay ở kết quả đầu tiên và không duyệt phần còn lại.

```
spent = [("ăn uống", 500), ("taxi", 300), ("ăn uống", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True, sau 500 không xét tiếp
biggest = max(spent, key=lambda p: p[1])    # ('ăn uống', 500)
```

Quy tắc chọn đơn giản: cần kết quả nhiều lần, lập chỉ mục hoặc `len()` — dùng list. Chỉ cần duyệt một lần và tính gì đó — dùng generator.

## Phạm vi biến: biến không rò rỉ ra ngoài

Câu hỏi khiến cả những người có kinh nghiệm cũng vấp:

```
x = 10
squares = [x for x in range(3)]
print(x)  # 10
```

Biến vòng lặp trong comprehension sống trong phạm vi riêng và không ghi đè biến bên ngoài (đúng với Python 3; ở Python 2 thì khác). Với vòng lặp `for` thông thường, sau vòng lặp `x` sẽ bằng 2. Từ phiên bản 3.12 (PEP 709), comprehension được gộp vào hàm chứa nó và chạy nhanh hơn, nhưng quy tắc phạm vi vẫn giữ nguyên.

## Giải nén * và ** trong comprehension: mới ở Python 3.15 (PEP 798)

Dấu sao để giải nén từ lâu đã dùng được trong lời gọi hàm và trong literal list, nhưng bên trong comprehension thì đây là `SyntaxError`. Từ Python 3.15, hạn chế này được gỡ bỏ: `*phần_tử` nghĩa là «chèn vào đây tất cả phần tử của nó». Cách này áp dụng cho list, set và generator, kể cả bất đồng bộ.

```
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # set
lazy = (*g for g in groups)        # generator lười
```

Dấu sao kép làm điều tương tự với dict. Điều này tiện cho cấu hình nhiều lớp «mặc định → tệp → cờ dòng lệnh»: mỗi lớp sau ghi đè các khóa của lớp trước.

```
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
```

Hai lưu ý từ khóa học. Dấu sao không được kết hợp với biểu thức điều kiện mà không có ngoặc: `[*w if w else 0 for w in weeks]` là lỗi cú pháp, cần đặt điều kiện trong ngoặc. Và đoạn code này sẽ không chạy trên 3.14 trở xuống: nếu thư viện của bạn cần hỗ trợ các phiên bản cũ, hãy giữ vòng `for` lồng nhau hoặc dùng `itertools.chain.from_iterable`.

## Generator bất đồng bộ

Nếu hàm `async def` có `yield`, bạn có một generator bất đồng bộ. Duyệt nó bằng `async for`, và bên trong có thể chờ mạng hoặc đĩa bằng `await`. Cũng có các comprehension bất đồng bộ.

```
import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())
```

## Lỗi thường gặp

- **Comprehension ba tầng.** Hai `for`, hai `if` và toán tử ba ngôi trên cùng một dòng sẽ không ai đọc nổi sau một tuần. Không vừa một dòng thì hãy viết vòng lặp thông thường: PEP 8 coi khả năng đọc quan trọng hơn sự ngắn gọn.
- **Duyệt lại generator.** `sum(gen)` lần hai sẽ trả về 0, vì generator đã cạn.
- **Comprehension chỉ để tạo hiệu ứng phụ.** `[print(x) for x in xs]` tạo một list `None` không cần thiết. Với các hành động, dùng vòng lặp `for`.
- **Kỳ vọng biểu thức generator là list.** `(x for x in xs)[0]` gây `TypeError`: generator không có chỉ số.

## Bài tập về generator để luyện tập

1. Viết generator `fib()` sinh vô hạn số Fibonacci, lấy 10 số đầu bằng `islice`.
2. Viết một dòng tạo dict «từ → độ dài» cho các từ dài hơn ba chữ cái.
3. Tính tổng bình phương các số chẵn đến một triệu mà không tạo list.
4. Viết generator `chunks(seq, n)` trả về các đoạn gồm `n` phần tử của một list.

## Tổng kết

Iterator trả về phần tử từng cái một qua `next()`, còn generator là cách đơn giản nhất để viết một iterator: một hàm có yield trong Python hoặc một biểu thức trong dấu ngoặc tròn. List, dict và set comprehension tạo ra tập hợp hoàn chỉnh trong một dòng; điều kiện lọc `if` đặt ở cuối, còn điều kiện chọn `if else` đặt ở đầu. Cần tiết kiệm bộ nhớ hoặc xử lý luồng dữ liệu một lần thì dùng generator; cần kết quả nhiều lần thì dùng list. Comprehension, generator và cú pháp giải nén mới của Python 3.15 được thực hành trong khóa học, lấy trình theo dõi chi tiêu làm ví dụ.

## FAQ

**Có thể dùng return trong generator không?**

Có. return không kèm giá trị chỉ kết thúc generator, còn return kèm giá trị sẽ đưa giá trị đó vào thuộc tính value của StopIteration; giá trị này nhận được từ biểu thức yield from.

**Làm sao biến generator thành list?**

Truyền nó vào list(): list(gen). Sau đó generator đã cạn, và toàn bộ giá trị nằm trong bộ nhớ.

**Nhanh hơn: list comprehension hay vòng lặp với append?**

Thường comprehension nhanh hơn một chút, vì không phải gọi phương thức append ở mỗi lượt. Nhưng nên chọn theo khả năng đọc; chênh lệch hiếm khi là yếu tố quyết định.

**Làm sao biết một đối tượng là generator?**

Dùng inspect.isgenerator(obj) cho đối tượng hoặc inspect.isgeneratorfunction(f) cho hàm; kiểm tra một đối tượng có phải iterator không bằng isinstance(obj, collections.abc.Iterator).

**Biểu thức generator khác list comprehension ở điểm nào?**

Dấu ngoặc vuông dựng toàn bộ list trong bộ nhớ ngay lập tức, còn dấu ngoặc tròn tạo một generator tính phần tử khi được yêu cầu và chỉ duyệt được một lần.


---
Source: Skillaria.Top (https://skillaria.top)
