Generators
generator คืออะไร?
หัวข้อที่มีชื่อว่า “generator คืออะไร?”generator คือ function ที่สามารถหยุดการทำงานชั่วคราว แล้วกลับมาทำงานต่อจากจุดที่หยุดไว้ได้ แทนที่จะคำนวณค่าทั้งหมดแล้ว return ออกมาเป็น list generator จะผลิตค่าออกมา ทีละค่า เฉพาะเมื่อถูกร้องขอเท่านั้น สิ่งนี้เรียกว่า lazy evaluation
หัวใจสำคัญคือ keyword yield
เมื่อ Python เจอ yield จะหยุด function ชั่วคราว ส่งค่ากลับไปให้ผู้เรียก แล้วแช่แข็ง state ภายในไว้ (ตัวแปรต่าง ๆ และตำแหน่งของ instruction pointer)
การเรียก next() ครั้งถัดไปจะทำงานต่อจากบรรทัดนั้นเป๊ะ ๆ
def count_up(start: int, stop: int): current = start while current <= stop: yield current # pause here and emit current current += 1 # resume here on the next next() call
gen = count_up(1, 4)print(next(gen)) # 1print(next(gen)) # 2print(next(gen)) # 3print(next(gen)) # 4# next(gen) would now raise StopIterationgenerator function เทียบกับ function ปกติ
หัวข้อที่มีชื่อว่า “generator function เทียบกับ function ปกติ”| ประเด็น | function ปกติ | generator function |
|---|---|---|
| Return | ครั้งเดียว ด้วย return | หลายครั้ง ด้วย yield |
| State | ถูกทิ้งหลัง return | ถูกแช่แข็งไว้ระหว่าง yield |
| Memory | สร้างผลลัพธ์ทั้งก้อน | ทีละค่า |
| Type | คือค่าที่ return | เป็น object ชนิด generator |
function จะกลายเป็น generator function ทันทีที่มีคำสั่ง yield แม้เพียงตัวเดียว
การเรียก generator function จะไม่ทำให้โค้ดทำงานเลย แต่จะ return generator object ออกมาทันที
iter() และ next()
หัวข้อที่มีชื่อว่า “iter() และ next()”ทุก generator object ทำตาม iterator protocol นั่นคือมี method __iter__() และ __next__()
next(gen)จะเลื่อนไปยังyieldตัวถัดไปและ return ค่านั้นออกมา- เมื่อไม่มีค่าเหลือแล้ว
next(gen)จะ raiseStopIteration - ลูป
forจะเรียกnext()ให้อัตโนมัติ และจับStopIterationเพื่อหยุด
def three_words(): yield "Python" yield "is" yield "expressive"
# Equivalent — for loop handles next() and StopIteration for youfor word in three_words(): print(word)generator ใช้ได้แค่ครั้งเดียว
หัวข้อที่มีชื่อว่า “generator ใช้ได้แค่ครั้งเดียว”generator object เป็น iterator ที่ใช้ได้ครั้งเดียว เมื่อทุก yield ทำงานจนจบและ function return แล้ว object นั้นจะว่างเปล่าตลอดไป การเรียก generator function ใหม่จะได้ generator object ใหม่ — แต่การ iterate object เดิมที่หมดแล้วเป็นครั้งที่สอง จะไม่ได้อะไรออกมาเลยแบบเงียบ ๆ โดยไม่มี error ใด ๆ:
def three_words(): yield "Python" yield "is" yield "expressive"
gen = three_words()print([w for w in gen]) # ['Python', 'is', 'expressive']print([w for w in gen]) # [] — same object, already exhausted, no error raisedสิ่งนี้ต่างจาก list ที่ loop ซ้ำได้กี่รอบก็ได้ ถ้า function ต้อง iterate ค่าเดียวกันมากกว่าหนึ่งครั้ง ให้ส่งเป็น list แทน (หรือเรียก generator function ใหม่เพื่อได้ generator สด ๆ) ไม่ใช่ generator object ที่ใช้ไปแล้ว
yield from: ส่งต่อไปยัง sub-generator
หัวข้อที่มีชื่อว่า “yield from: ส่งต่อไปยัง sub-generator”yield from ส่งการควบคุมไปให้ iterable อีกตัวหนึ่ง โดยส่งต่อทุกค่าที่มันผลิตออกมาราวกับว่า generator ตัวนอกเป็นคน yield เอง — และมันยังประเมินค่าเป็น return value ของ sub-generator ด้วย:
def inner(): yield 1 yield 2 return "inner done"
def outer(): result = yield from inner() # forwards 1, then 2 print("inner returned:", result) yield 3
print(list(outer()))# inner returned: inner done# [1, 2, 3]yield from เป็นวิธีมาตรฐานในการประกอบ generator เข้าด้วยกัน — สั้นกว่าและส่งต่อ .send(), .throw() และ return value ของ sub-generator ได้ถูกต้อง ซึ่งลูป for x in inner(): yield x แบบเขียนเองไม่ทำให้เลยสักอย่าง
.send(): การสื่อสารสองทาง
หัวข้อที่มีชื่อว่า “.send(): การสื่อสารสองทาง”yield ไม่ใช่แค่จุดออกเท่านั้น — มันเป็น expression ที่รับค่าที่ถูกส่งเข้ามาผ่าน .send() ได้ด้วย การเรียก next() (หรือ .send(None)) ครั้งแรกจำเป็นต้องมีก่อนเสมอ เพื่อขับ generator ไปถึง yield ตัวแรก (priming) ก่อนที่จะส่งค่าจริงเข้าไปได้:
def echo_upper(): received = None while True: received = yield received # yield is an expression here if received is not None: received = received.upper()
gen = echo_upper()print(next(gen)) # None — priming call, runs up to the first yieldprint(gen.send("hello")) # HELLOprint(gen.send("world")) # WORLDreturn ใน generator กับ StopIteration.value
หัวข้อที่มีชื่อว่า “return ใน generator กับ StopIteration.value”statement return ภายใน generator function ไม่ได้ส่งค่ากลับแบบปกติ — มันยังคง raise StopIteration เหมือนเดิม แต่จะแนบค่าที่ return ไว้ที่ attribute .value ของ exception นั้น ลูป for ธรรมดาจะไม่มีวันเห็นค่านี้ (มันแค่หยุด) แต่การ iterate ด้วยมือหรือ yield from ดึงค่ามาได้:
def summing(): total = 0 while True: x = yield if x is None: return total total += x
gen = summing()next(gen)gen.send(10)gen.send(20)try: gen.send(None)except StopIteration as e: print(e.value) # 30.close() และ finally
หัวข้อที่มีชื่อว่า “.close() และ finally”การเรียก .close() บน generator จะ raise GeneratorExit ข้างในตรงจุดที่มันหยุดชั่วคราวอยู่ — นี่คือสิ่งที่ทำให้ block try/finally รอบ yield ทำงาน cleanup ได้ เหมือนกับ guarantee ที่ with block ให้กับ context manager เป๊ะ ๆ:
def resource_gen(): print("acquiring resource") try: yield 1 yield 2 finally: print("releasing resource")
gen = resource_gen()print(next(gen)) # acquiring resource \n 1gen.close() # releasing resourcegenerator ที่ถูก garbage collect ก่อนจะถูกใช้จนหมด ก็จะถูกเรียก .close() ให้อัตโนมัติเช่นกัน ดังนั้น block finally ก็ยังทำงานอยู่ — แต่การพึ่งจังหวะของ garbage collection สำหรับ cleanup นั้นเปราะบาง ให้เรียก .close() เอง (หรือขับ generator ผ่าน with contextlib.closing(gen):) เมื่อรู้ล่วงหน้าว่าจะจบก่อนกำหนด
Type hint สำหรับ generator
หัวข้อที่มีชื่อว่า “Type hint สำหรับ generator”generator ที่ใช้แค่ค่าที่ yield ออกมา annotate เป็น Iterator[YieldType] ได้เลย เมื่อ .send() หรือ return value มีความหมายด้วย Generator[YieldType, SendType, ReturnType] แบบเต็มสามพารามิเตอร์จะบอกชัดเจนว่าอะไรไหลไปทางไหนบ้าง:
from typing import Generator, Iterator
def count_up(start: int, stop: int) -> Iterator[int]: current = start while current <= stop: yield current current += 1
def echo_typed() -> Generator[str, str, str]: received = "" while True: received = yield received.upper() if received == "STOP": return "done"Iterator[int] คือรูปย่อของ “yield เป็น int, ไม่สนใจว่าจะถูกส่งอะไรเข้ามา, ไม่สนใจ return value” — ให้ใช้แบบนี้ เว้นแต่ generator นั้นใช้ .send() หรือมี return ที่มีความหมายจริง ๆ
generator เทียบกับ list: memory
หัวข้อที่มีชื่อว่า “generator เทียบกับ list: memory”generator ไม่ materialize ค่าของมันเลย — มันเก็บแค่ state ที่พอจะผลิตค่า ถัดไป เท่านั้น sys.getsizeof ทำให้ความต่างนี้เห็นเป็นรูปธรรม: ขนาดของ generator คงที่ไม่ว่าจะผลิตค่าไปกี่ตัวในที่สุด ในขณะที่ขนาดของ list โตขึ้นตามทุก element:
import sys
big_list = [x for x in range(100_000)]big_gen = (x for x in range(100_000))print(sys.getsizeof(big_list)) # 800984 (bytes; grows with element count)print(sys.getsizeof(big_gen)) # 200 (bytes; constant, regardless of range size)นี่คือเหตุผลที่ sum(x * x for x in range(10_000_000)) ไม่ต้องสร้าง list สิบล้าน element ขึ้นมาก่อนเลย — generator expression ป้อนค่าให้ sum() ทีละตัว
itertools.chain และ itertools.islice
หัวข้อที่มีชื่อว่า “itertools.chain และ itertools.islice”itertools.islice ตัดช่วง (slice) ของ iterable ใด ๆ ก็ได้ — รวมถึง generator ที่ไม่รู้จบ — โดยไม่ต้องเรียก list() กับมันก่อนเลย ซึ่งจะไม่มีวัน return itertools.chain iterate หลาย iterable ต่อกันราวกับเป็นตัวเดียว โดยไม่ต้องต่อ (concatenate) ให้กลายเป็น sequence ใหม่:
from itertools import islice, chain
def fibonacci(): a, b = 0, 1 while True: # infinite loop — but never blocks yield a a, b = b, a + b
first_5 = list(islice(fibonacci(), 5))print(first_5) # [0, 1, 1, 2, 3]
combined = list(chain([1, 2], (3, 4), range(5, 7)))print(combined) # [1, 2, 3, 4, 5, 6]generator expression
หัวข้อที่มีชื่อว่า “generator expression”เช่นเดียวกับที่ list comprehension สร้าง list generator expression ก็สร้าง generator โดยใช้วงเล็บแทนวงเล็บเหลี่ยม วิธีนี้ประหยัด memory เพราะไม่มีการสร้าง list ขึ้นมาจริง
squares_list = [x ** 2 for x in range(1_000_000)] # allocates a listsquares_gen = (x ** 2 for x in range(1_000_000)) # allocates almost nothing
# Consume lazilyfor sq in squares_gen: if sq > 25: breakgenerator expression เข้ากันได้อย่างเป็นธรรมชาติกับ built-in ที่รับ iterable:
total = sum(x ** 2 for x in range(10)) # no brackets needed inside sum()generator แบบไม่รู้จบ
หัวข้อที่มีชื่อว่า “generator แบบไม่รู้จบ”เนื่องจาก generator เป็นแบบ lazy เราจึงสามารถนิยามลำดับที่ไม่มีวันจบได้ ตราบเท่าที่ฝั่งผู้บริโภคหยุดร้องขอ
def fibonacci(): a, b = 0, 1 while True: # infinite loop — but never blocks yield a a, b = b, a + bเดโมที่รันได้แบบเต็ม
หัวข้อที่มีชื่อว่า “เดโมที่รันได้แบบเต็ม”from itertools import islice
def fibonacci(): """Infinite Fibonacci sequence generator.""" a, b = 0, 1 while True: yield a a, b = b, a + b
# Take only the first 10 values — safe because islice stops earlyfirst_10 = list(islice(fibonacci(), 10))print("First 10 Fibonacci numbers:")print(first_10)
# Generator expression: squares of even numberseven_squares = (x * x for x in range(1, 11) if x % 2 == 0)print("\nSquares of even numbers 1-10:")for val in even_squares: print(val, end=" ")print()
# Demonstrate single-use naturegen = (x * 3 for x in range(5))print("\nFirst pass:", list(gen))print("Second pass:", list(gen)) # exhausted — empty list
# Manual next() / StopIterationcounter = iter(range(3))print("\nManual iteration:")print(next(counter))print(next(counter))print(next(counter))try: print(next(counter))except StopIteration: print("StopIteration raised — generator exhausted")Loading Python runtime (first run only)…
เดโมเจาะลึก: yield from, .send(), .close() และ memory
หัวข้อที่มีชื่อว่า “เดโมเจาะลึก: yield from, .send(), .close() และ memory”import sysfrom itertools import islice, chainfrom typing import Generator, Iterator
# --- exhausted once: second loop is empty ---def three_words(): yield "Python" yield "is" yield "expressive"
gen = three_words()print("--- exhausted once ---")print("first pass:", [w for w in gen])print("second pass:", [w for w in gen])
# --- yield from ---def inner(): yield 1 yield 2 return "inner done"
def outer_yield_from(): result = yield from inner() print("inner returned:", result) yield 3
print("\n--- yield from ---")print(list(outer_yield_from()))
# --- .send() ---def echo_upper(): received = None while True: received = yield received if received is not None: received = received.upper()
print("\n--- .send() ---")gen2 = echo_upper()print(next(gen2)) # prime the generator, prints Noneprint(gen2.send("hello")) # HELLOprint(gen2.send("world")) # WORLD
# --- return value via StopIteration.value ---def summing(): total = 0 while True: x = yield if x is None: return total total += x
print("\n--- return + StopIteration.value ---")gen3 = summing()next(gen3)gen3.send(10)gen3.send(20)try: gen3.send(None)except StopIteration as e: print("StopIteration.value:", e.value)
# --- .close() / finally ---def resource_gen(): print("acquiring resource") try: yield 1 yield 2 finally: print("releasing resource")
print("\n--- .close() / finally ---")gen4 = resource_gen()print(next(gen4))gen4.close()try: next(gen4)except StopIteration: print("StopIteration after close()")
# --- type hints: Iterator[int] / Generator[Y, S, R] ---def count_up(start: int, stop: int) -> Iterator[int]: current = start while current <= stop: yield current current += 1
def echo_typed() -> Generator[str, str, str]: received = "" while True: received = yield received.upper() if received == "STOP": return "done"
print("\n--- typed generators still run the same ---")print(list(count_up(1, 3)))
# --- generator vs list memory ---print("\n--- memory: list vs generator ---")big_list = [x for x in range(100_000)]big_gen = (x for x in range(100_000))print("list getsizeof:", sys.getsizeof(big_list))print("generator getsizeof:", sys.getsizeof(big_gen))
# --- itertools.chain / islice ---print("\n--- itertools.chain / islice ---")def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b
first_5 = list(islice(fibonacci(), 5))print("first 5 fib:", first_5)
combined = list(chain([1, 2], (3, 4), range(5, 7)))print("chained:", combined)Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”generator คือ state machine เล็ก ๆ ที่มี state เหล่านี้เป๊ะ ๆ — เริ่มที่ Created แล้วทุก next()/.send() จะเลื่อนมันไปข้างหน้า จนกว่าจะถึง Returned (raise StopIteration พร้อม return value) หรือ Closed (ผ่าน .close() ที่รัน block finally ระหว่างทาง):
stateDiagram-v2 [*] --> Created: call the generator function Created --> Suspended: first next()/send(None) Suspended --> Suspended: next()/send(v) resumes to the next yield Suspended --> Returned: function body reaches return or falls off the end Suspended --> Closed: .close() raises GeneratorExit at the paused yield Returned --> [*]: StopIteration(value=...) Closed --> [*]: finally blocks have already run
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| generator (lazy) | memory คงที่ไม่ว่าจะผลิตค่ากี่ตัว (sys.getsizeof อยู่ที่ ~200 byte เสมอ) | ใช้ได้ครั้งเดียว — iterate รอบสองจะไม่ได้อะไรเลยแบบเงียบ ๆ ไม่มี error |
| list (eager) | iterate ซ้ำได้กี่รอบก็ได้ รองรับ index และ len() | memory โตขึ้นตามทุก element — ต้องใส่ทั้งหมดไว้ใน memory พร้อมกัน |
yield from | ส่งต่อค่า, .send() และ return value ของ sub-generator ได้ถูกต้อง | มีความหมายเฉพาะตอน delegate ไปให้ generator/iterable อีกตัว ไม่ใช่ loop body ธรรมดา |
generator สองทางด้วย .send() | ให้ generator รับค่าได้ ไม่ใช่แค่ผลิตค่าอย่างเดียว (สไตล์ coroutine) | ต้องมี next() priming ก่อนเสมอ และ flow ตามยากกว่า generator แบบผลิตอย่างเดียว |
itertools.islice/chain บน generator | ตัดช่วงหรือรวม sequence ที่ไม่รู้จบ/lazy โดยไม่ต้อง materialize | consume generator ต้นทาง — “ย้อนกลับ” ผ่านสิ่งที่ islice ดึงไปแล้วไม่ได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “iterate generator ที่หมดแล้วจะ raise error” — ข้อเท็จจริง: มันผลิตค่าเป็นศูนย์ตัวแบบเงียบ ๆ ไม่มี exception ใด ๆ — ไม่มี
StopIterationรั่วออกมาจากforloop หรือ list comprehension เลยไม่ว่ากรณีไหน จึงสังเกตพลาดได้ง่ายตอน review - “
yield from inner()เหมือนกับfor x in inner(): yield x” — ข้อเท็จจริง:yield fromยังส่งต่อ.send()/.throw()เข้าไปในinner()และประเมินค่าเป็น return value ของinner()ด้วย ในขณะที่ loop แบบเขียนเองไม่ทำอย่างใดอย่างหนึ่งเลย - “
return valueของ generator คือสิ่งที่forloop ได้รับตอนจบ” — ข้อเท็จจริง:forloop ไม่มีวันเห็นค่านี้เลย — มันถูกแนบไว้ที่StopIteration.valueดึงออกมาได้แค่ผ่านการ iterate ด้วยมือหรือyield fromเท่านั้น - “
.close()แค่หยุด generator โดยไม่มี cleanup” — ข้อเท็จจริง: มัน raiseGeneratorExitตรงจุดที่หยุดชั่วคราวอยู่ ดังนั้นtry/finallyรอบมันก็ยังทำงาน เหมือนการปิด context manager เป๊ะ ๆ - “generator expression ก็เหมือน list แค่เขียนด้วย
()” — ข้อเท็จจริง:sys.getsizeofแสดงว่าขนาดของ generator คงที่ (~200 byte) ไม่ว่า range จะใหญ่แค่ไหน ในขณะที่ขนาดของ list โตตามจำนวน element — generator ยังไม่ได้คำนวณหรือเก็บค่าไหนไว้เลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- generator (ไม่ใช่ list) ถูก iterate มากกว่าหนึ่งครั้งหรือเปล่า — ใน loop แยกกันสองที่ หรือถูกส่งให้ function สองตัวที่ต่างก็ loop ผ่านมัน? ตัวที่บริโภคเป็นตัวที่สองจะได้อะไรกลับมาเลยแบบเงียบ ๆ
- code delegate ไปยัง sub-generator ด้วย
for x in sub(): yield xแบบเขียนเอง ทั้งที่ return value หรือพฤติกรรม.send()ของมันมีความหมายจริง ๆ หรือเปล่า? ตรงนั้นควรเป็นyield from sub() return <value>ของ generator ดูเหมือนตั้งใจให้ไปถึงforloop หรือเปล่า? มันไปถึงแค่StopIteration.valueเท่านั้น —forloop ธรรมดาจะทิ้งมันไปแบบเงียบ ๆ- resource (file, lock, connection) ถูกเปิดก่อน
yieldโดยไม่มีtry/finallyหรือเปล่า? ถ้า generator ถูกทิ้งกลางทาง.close()(หรือ garbage collection) ก็ยัง raiseGeneratorExitที่yieldนั้น และถ้าไม่มีfinallyresource จะไม่ถูกปล่อยเลย list(...)ถูกเรียกกับ generator ที่ไม่รู้จบ หรือที่ในทางปฏิบัติไม่มีขอบเขตบนหรือเปล่า? ควรถูกห่อด้วยitertools.islice(..., n)ก่อน
🐛 หา bug ในโค้ดนี้
def read_lines(path): with open(path, encoding="utf-8") as f: for line in f: yield line.strip()
def process(path): lines = read_lines(path) total_lines = sum(1 for _ in lines) non_empty = [line for line in lines if line] print(f"{path}: {total_lines} lines, {len(non_empty)} non-empty")เฉลย
lines เป็น generator และ sum(1 for _ in lines) consume มันจนหมดขณะนับ พอถึงจังหวะที่ list comprehension [line for line in lines if line] รัน lines ก็หมดสภาพไปแล้ว ดังนั้น non_empty จึงเป็น [] เสมอไม่ว่าไฟล์จะมีเนื้อหาอะไร — ไม่มี exception ใด ๆ ถูก raise เลย bug นี้จึงหลุดออกไปแบบเงียบ ๆ และโผล่มาเป็นแค่ตัวเลขนับที่ผิด ยืนยันแล้วบน Python 3.14: การพิมพ์ list(lines) เป็นครั้งที่สองหลังเรียก sum(...) จะได้ []. ทางแก้คือ iterate generator แค่ครั้งเดียว — อ่านมันเข้า list ก่อน (lines = list(read_lines(path))) ถ้าต้องสแกนมากกว่าหนึ่งครั้ง หรือคำนวณทั้ง total_lines และ non_empty ใน pass เดียวผ่าน generator นั้น