# Generator ใน Python: yield, iterator และ list/dict comprehension

URL: https://skillaria.top/th/guides/python/python-generators

Generator ใน Python คืออ็อบเจกต์ที่ให้ค่าทีละตัวเมื่อถูกขอ โดยไม่เก็บทั้งลำดับไว้ในหน่วยความจำ สร้างได้ด้วยฟังก์ชันที่มีคำสั่ง yield หรือนิพจน์ generator ในวงเล็บ ส่วน list, dict และ set comprehension เป็นรูปแบบสั้นสำหรับสร้างคอลเลกชันสำเร็จรูปในบรรทัดเดียว เช่น [x * 2 for x in xs]

Generator ใน Python คืออ็อบเจกต์ที่ให้ค่าทีละตัวเมื่อมีการขอ และไม่เก็บลำดับทั้งหมดไว้ในหน่วยความจำ สร้างได้สองวิธี คือฟังก์ชันที่มีคำสั่ง `yield` หรือนิพจน์ generator ในวงเล็บ ส่วน list comprehension, dict comprehension และ set comprehension คือรูปแบบสั้น ๆ สำหรับสร้างคอลเลกชันสำเร็จรูปในบรรทัดเดียว บทความนี้จะอธิบายการทำงานของ python yield ความต่างระหว่าง generator กับ iterator และวิธีเขียน comprehension ให้ยังอ่านรู้เรื่อง

## Iterator: หัวใจของลูป for

ก่อนจะเข้าใจ generator ต้องเข้าใจ iterator ก่อน Iterable คือสิ่งที่วนลูปได้ เช่น ลิสต์ สตริง ดิกชันนารี หรือไฟล์ ส่วน iterator คืออ็อบเจกต์ที่จำตำแหน่งปัจจุบันไว้ และเมื่อเรียก `next()` จะให้สมาชิกตัวถัดไป เมื่อสมาชิกหมดจะโยน `StopIteration`

```
nums = [10, 20, 30]
it = iter(nums)      # ขอ iterator จากลิสต์
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration
```

ลูป `for` ทำหน้าที่นี้เองทั้งหมด คือเรียก `iter()` แล้วเรียก `next()` ซ้ำจนเจอ `StopIteration` เราสามารถเขียน iterator ของตัวเองเป็นคลาสที่มีเมธอด `__iter__` และ `__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] — ทำต่อจากจุดที่หยุดไว้
```

เมื่อฟังก์ชันทำงานจนจบ (หรือถึง `return`) generator จะโยน `StopIteration` เองโดยอัตโนมัติ และลูป `for` จะจบอย่างเรียบร้อย ความต่างสำคัญระหว่าง yield กับ return คือ return จบการทำงานของฟังก์ชันและคืนค่าเดียว ส่วน yield หยุดฟังก์ชันชั่วคราวและให้ค่าได้หลายครั้งตามต้องการ

### ความต่างระหว่าง generator กับ iterator

Generator ทุกตัวเป็น iterator เพราะมีทั้ง `__iter__` และ `__next__` แต่ iterator ไม่ได้เป็น generator ทุกตัว ความต่างอยู่ที่วิธีสร้างและความสามารถ

|  | Iterator (คลาส) | Generator |
| --- | --- | --- |

| วิธีสร้าง | คลาสที่มี `__iter__` และ `__next__` | ฟังก์ชันที่มี `yield` หรือ `(x for x in ...)` |

| สถานะ | ต้องเก็บเองในแอตทริบิวต์ | เก็บอัตโนมัติในตัวแปรภายในฟังก์ชัน |

| StopIteration | ต้องโยนเอง | เกิดขึ้นเองเมื่อฟังก์ชันจบ |

| ความสามารถเพิ่มเติม | ขึ้นกับที่เขียน | `send()` `throw()` `close()` |

ทั้งสองแบบมีคุณสมบัติสำคัญร่วมกันคือ วนได้เพียงครั้งเดียว การใช้ `for` ซ้ำกับ generator ที่วนหมดแล้วจะไม่ได้ค่าอะไรเลย ต้องสร้าง generator ใหม่

### Generator ที่ไม่มีที่สิ้นสุดและการอ่านไฟล์ขนาดใหญ่

Generator ไม่จำเป็นต้องจบ ลำดับที่ไม่มีที่สิ้นสุดใช้ได้อย่างปลอดภัยตราบที่ดึงมาเฉพาะจำนวนจำกัด:

```
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']
```

การใช้งานจริงที่พบบ่อยที่สุดคือประมวลผลไฟล์ขนาดใหญ่ทีละบรรทัด Generator สามารถต่อกันเป็นสายได้ ทำให้ในหน่วยความจำมีเพียงหนึ่งบรรทัดในแต่ละช่วงเวลา ไม่ใช่ทั้งไฟล์:

```
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 และ send

`yield from` ส่งต่อค่าทั้งหมดจากอ็อบเจกต์ที่วนซ้ำได้อีกตัว ช่วยให้ไม่ต้องเขียนลูปซ้อน:

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

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

เมธอด `send()` ส่งค่าเข้าไปยังภายใน generator ค่านั้นจะกลายเป็นผลของนิพจน์ `yield` ก่อนการเรียก `send` ครั้งแรกต้อง "สตาร์ต" generator ด้วย `next()` ก่อน ตัวอย่างต่อไปนี้เป็นตัวคำนวณค่าเฉลี่ยแบบสะสม:

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

a = averager()
next(a)            # ไปถึง yield แรก
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0
```

ในโค้ดงานประจำ `send` แทบไม่ค่อยได้ใช้ แต่ถูกถามบ่อยในการสัมภาษณ์งาน

### range เป็น generator หรือไม่

ไม่ใช่ `range` เป็นลำดับแบบขี้เกียจ (lazy) เช่นกัน คือไม่เก็บตัวเลขทั้งหมดไว้ แต่รองรับการเข้าถึงด้วยดัชนี `len()` การตรวจ `in` และการวนซ้ำได้หลายครั้ง ซึ่ง generator ทำไม่ได้

```
r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # ได้ [0, 1, 2, 3, 4] ทั้งสองครั้ง
```

## List comprehension

List comprehension มีรูปแบบ `[นิพจน์ for x in คอลเลกชัน]` ซึ่งสร้างลิสต์ใหม่โดยไม่แก้ไขลิสต์เดิม อ่านจากซ้ายไปขวา: ก่อนอื่นบอกว่าจะเก็บอะไร แล้วบอกว่าหยิบจากที่ไหน

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

# ลูปธรรมดา
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# เขียนใหม่ด้วย comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]
```

แม้จะเรียกว่า "generator ของลิสต์" ผลลัพธ์เป็นลิสต์สำเร็จรูปธรรมดา ไม่ใช่ generator เพราะทุกสมาชิกถูกคำนวณทันที

### List comprehension ที่มี if และ if else

จุดนี้เป็นสิ่งที่สับสนบ่อยที่สุด เงื่อนไขแบบกรองจะวางไว้ท้ายและทิ้งสมาชิกที่ไม่ตรง ส่วนเงื่อนไขแบบเลือก (ternary) จะวางไว้ต้นนิพจน์และเปลี่ยนสมาชิกทุกตัวให้เป็นค่าใดค่าหนึ่งจากสองค่า โดยไม่ทิ้งสมาชิกใด

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

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["มาก" if a > 200 else "น้อย" for a in amounts]
# ['น้อย', 'มาก', 'มาก']

# กรองและเลือกค่าไปพร้อมกัน
odd_marks = ["คู่" if n % 2 == 0 else "คี่" for n in range(6) if n > 2]
# ['คี่', 'คู่', 'คี่']
```

การเขียน `[a for a in amounts if a > 200 else 0]` เป็นข้อผิดพลาดด้านไวยากรณ์ เพราะ `else` ใช้ได้เฉพาะเงื่อนไขที่อยู่ต้นนิพจน์เท่านั้น

### comprehension ซ้อนกันและลิสต์สองมิติ

`for` ที่ซ้อนกันเขียนตามลำดับเดียวกับลูปซ้อน จากบนลงล่าง ช่วยไม่ให้สับสนลำดับเมื่อรวมลิสต์ของลิสต์:

```
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# เทียบเท่ากับ:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]
```

ลิสต์สองมิติ (เมทริกซ์) สร้างได้ด้วย comprehension ซ้อนใน comprehension ข้อผิดพลาดคลาสสิกคือการคูณลิสต์ ซึ่งทำให้ทุกแถวกลายเป็นอ็อบเจกต์เดียวกัน

```
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — ทั้งสองแถวเปลี่ยนไปหมด

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

# การสลับแถวกับคอลัมน์ (transpose)
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 และ set comprehension

หลักการเดียวกันใช้ได้กับคอลเลกชันอื่น วงเล็บปีกกาที่มีเครื่องหมายโคลอนสร้างดิกชันนารี (dict comprehension) ส่วนที่ไม่มีโคลอนสร้างเซต (set comprehension)

```
spent = [("อาหาร", 500), ("แท็กซี่", 300), ("อาหาร", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'อาหาร', 'แท็กซี่'} — เซตไม่รับประกันลำดับ

last = {cat: amount for cat, amount in spent}
print(last)        # {'อาหาร': 200, 'แท็กซี่': 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()}
```

สังเกต `last`: เมื่อคีย์ซ้ำ ค่าสุดท้ายจะชนะ ทำให้ยอด 500 หายไป หากต้องการรวมค่า comprehension ไม่เหมาะ ให้เขียนลูปหรือใช้ `collections.Counter` / `defaultdict`

### แล้ว tuple comprehension ล่ะ

Python ไม่มี "tuple comprehension" แบบแยกต่างหาก วงเล็บกลมให้ generator expression แต่หากต้องการทูเพิล ให้ส่ง generator เข้า `tuple()`:

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

## Generator expression: ไม่เก็บ แต่ให้ทีละค่า

หากเปลี่ยนวงเล็บเหลี่ยมเป็นวงเล็บกลม จะได้ generator expression ซึ่งไม่คำนวณล่วงหน้า แต่ให้ค่าทีละตัว สำหรับข้อมูลหนึ่งล้านรายการ ความต่างด้านหน่วยความจำมหาศาล:

```
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))  # ราว 8 MB
print(sys.getsizeof(as_gen))   # ไม่กี่ร้อยไบต์
```

Generator expression มักใช้ร่วมกับฟังก์ชันที่รวมค่า หากมันเป็นอาร์กิวเมนต์เดียวของฟังก์ชัน ไม่ต้องใส่วงเล็บซ้อนเพิ่ม และ `any()` กับ `all()` จะหยุดทันทีที่ได้คำตอบ โดยไม่วนต่อ

```
spent = [("อาหาร", 500), ("แท็กซี่", 300), ("อาหาร", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True และไม่ดูรายการต่อ
biggest = max(spent, key=lambda p: p[1])    # ('อาหาร', 500)
```

กฎในการเลือกง่ายมาก หากต้องใช้ผลลัพธ์หลายครั้ง หรือต้องการ indexing และ `len()` ให้ใช้ลิสต์ หากวนผ่านเพียงครั้งเดียวเพื่อคำนวณบางอย่าง ให้ใช้ generator

## ขอบเขตของตัวแปร: ไม่รั่วออกนอก comprehension

คำถามที่ทำให้แม้แต่นักพัฒนาที่มีประสบการณ์สะดุดได้:

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

ตัวแปรลูปภายใน comprehension อยู่ในขอบเขตของตัวเอง จึงไม่ทับตัวแปรภายนอก (ใน Python 3 ซึ่งต่างจาก Python 2) ส่วนลูป `for` ธรรมดา หลังจากวนแล้ว `x` จะเท่ากับ 2 ตั้งแต่ Python 3.12 (PEP 709) comprehension ถูกรวมเข้ากับฟังก์ชันที่ล้อมอยู่เพื่อให้เร็วขึ้น แต่กฎเรื่องขอบเขตยังเหมือนเดิม

## การแตกค่า * และ ** ใน comprehension: ของใหม่ใน Python 3.15 (PEP 798)

เครื่องหมายดอกจันสำหรับการแตกค่าใช้ได้มานานแล้วในการเรียกฟังก์ชันและลิเทอรัลลิสต์ แต่ภายใน comprehension เคยเป็น `SyntaxError` ใน Python 3.15 ข้อจำกัดนี้ถูกยกเลิก `*element` หมายถึง "ใส่สมาชิกทั้งหมดของมันเข้าที่นี่" ใช้ได้กับลิสต์ เซต และ generator รวมถึง generator แบบอะซิงโครนัส

```
# 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}        # เซต
lazy = (*g for g in groups)        # generator แบบขี้เกียจ
```

เครื่องหมายดอกจันสองตัว `**` ทำงานเช่นเดียวกันกับดิกชันนารี เหมาะกับการตั้งค่าแบบหลายชั้น "ค่าเริ่มต้น → ไฟล์ → แฟล็ก" โดยแต่ละชั้นถัดไปจะทับคีย์ของชั้นก่อนหน้า

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

มีสองข้อควรระวังจากคอร์ส ประการแรก ห้ามใช้เครื่องหมายดอกจันร่วมกับนิพจน์เงื่อนไขโดยไม่มีวงเล็บ เช่น `[*w if w else 0 for w in weeks]` เป็นข้อผิดพลาดด้านไวยากรณ์ ต้องใส่วงเล็บครอบเงื่อนไข ประการที่สอง โค้ดนี้จะรันไม่ได้บน Python 3.14 และเวอร์ชันก่อนหน้า หากไลบรารีของคุณรองรับเวอร์ชันเก่า ให้ใช้ `for` ซ้อนแบบเดิมหรือ `itertools.chain.from_iterable`

## Generator แบบอะซิงโครนัส

หากฟังก์ชัน `async def` มี `yield` จะได้ generator แบบอะซิงโครนัส วนผ่านด้วย `async for` และภายในสามารถรอเครือข่ายหรือดิสก์ด้วย `await` ได้ มี async comprehension ด้วย

```
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())
```

## ข้อผิดพลาดที่พบบ่อย

- **comprehension ซ้อนสามชั้น** มี `for` สองตัว `if` สองตัว และ ternary ในบรรทัดเดียว ไม่มีใครอ่านรู้เรื่องหลังผ่านไปหนึ่งสัปดาห์ หากเขียนในบรรทัดเดียวไม่ชัด ให้ใช้ลูปธรรมดา เพราะ PEP 8 ให้ความสำคัญกับความอ่านง่ายมากกว่าความกระชับ
- **วนซ้ำ generator ที่หมดแล้ว** การเรียก `sum(gen)` ครั้งที่สองจะได้ 0 เพราะ generator หมดไปแล้ว
- **ใช้ comprehension เพื่อผลข้างเคียง** `[print(x) for x in xs]` สร้างลิสต์ของ `None` ที่ไม่มีประโยชน์ สำหรับการกระทำให้ใช้ลูป `for`
- **คาดหวังว่า generator expression จะ indexing ได้** `(x for x in xs)[0]` เกิด `TypeError` เพราะ generator ไม่มีดัชนี

## โจทย์ฝึกเกี่ยวกับ generator

1. เขียน generator `fib()` ที่ให้เลขฟีโบนัชชีไม่สิ้นสุด แล้วดึง 10 ตัวแรกด้วย `islice`
2. เขียนในบรรทัดเดียวเพื่อได้ดิกชันนารี "คำ → ความยาว" สำหรับคำที่ยาวกว่าสามตัวอักษร
3. คำนวณผลรวมของกำลังสองของจำนวนคู่ถึงหนึ่งล้าน โดยไม่สร้างลิสต์
4. เขียน generator `chunks(seq, n)` ที่ให้ลิสต์ย่อยทีละ `n` สมาชิก

## สรุป

Iterator ให้สมาชิกทีละตัวผ่าน `next()` ส่วน generator คือวิธีที่ง่ายที่สุดในการเขียนมัน ไม่ว่าจะเป็นฟังก์ชันที่มี python yield หรือนิพจน์ในวงเล็บกลม list, dict และ set comprehension สร้างคอลเลกชันสำเร็จรูปในบรรทัดเดียว โดยเงื่อนไข `if` ที่กรองวางไว้ท้าย และ `if else` ที่เลือกค่าวางไว้ต้นนิพจน์ ต้องการประหยัดหน่วยความจำหรือประมวลผลข้อมูลแบบสตรีมครั้งเดียว ใช้ generator ต้องการผลลัพธ์หลายครั้ง ใช้ลิสต์ comprehension, generator และการแตกค่าใหม่จาก Python 3.15 ถูกสาธิตจริงในคอร์สผ่านตัวอย่างโปรแกรมติดตามค่าใช้จ่าย

## FAQ

**ใน generator ใช้ return ได้หรือไม่?**

ได้ return ที่ไม่มีค่าจะจบ generator ส่วน return ที่มีค่าจะเก็บค่านั้นไว้ในแอตทริบิวต์ value ของ StopIteration ซึ่งนิพจน์ yield from จะได้รับค่านั้น

**จะแปลง generator เป็นลิสต์ได้อย่างไร?**

ส่งเข้า list() เช่น list(gen) แต่หลังจากนั้น generator จะหมดแล้ว และทุกค่าจะอยู่ในหน่วยความจำ

**list comprehension กับลูปที่ใช้ append อันไหนเร็วกว่า?**

โดยทั่วไป comprehension เร็วกว่าเล็กน้อย เพราะไม่ต้องเรียกเมธอด append ในทุกรอบ แต่ควรเลือกตามความอ่านง่าย เพราะความต่างมักไม่ชี้ขาด

**จะรู้ได้อย่างไรว่าอ็อบเจกต์เป็น generator?**

ใช้ inspect.isgenerator(obj) สำหรับอ็อบเจกต์ หรือ inspect.isgeneratorfunction(f) สำหรับฟังก์ชัน ส่วนการตรวจว่าเป็น iterator ทำได้ด้วย isinstance(obj, collections.abc.Iterator)

**generator expression ต่างจาก list comprehension อย่างไร?**

วงเล็บเหลี่ยมสร้างลิสต์ทั้งก้อนในหน่วยความจำทันที ส่วนวงเล็บกลมสร้าง generator ที่คำนวณค่าเมื่อถูกขอ และวนได้เพียงรอบเดียว


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