ข้ามไปยังเนื้อหา

Generators

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)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
print(next(gen)) # 4
# next(gen) would now raise StopIteration
ประเด็นfunction ปกติgenerator function
Returnครั้งเดียว ด้วย returnหลายครั้ง ด้วย yield
Stateถูกทิ้งหลัง returnถูกแช่แข็งไว้ระหว่าง yield
Memoryสร้างผลลัพธ์ทั้งก้อนทีละค่า
Typeคือค่าที่ returnเป็น object ชนิด generator

function จะกลายเป็น generator function ทันทีที่มีคำสั่ง yield แม้เพียงตัวเดียว การเรียก generator function จะไม่ทำให้โค้ดทำงานเลย แต่จะ return generator object ออกมาทันที

ทุก generator object ทำตาม iterator protocol นั่นคือมี method __iter__() และ __next__()

  • next(gen) จะเลื่อนไปยัง yield ตัวถัดไปและ return ค่านั้นออกมา
  • เมื่อไม่มีค่าเหลือแล้ว next(gen) จะ raise StopIteration
  • ลูป for จะเรียก next() ให้อัตโนมัติ และจับ StopIteration เพื่อหยุด
def three_words():
yield "Python"
yield "is"
yield "expressive"
# Equivalent — for loop handles next() and StopIteration for you
for word in three_words():
print(word)

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 ส่งการควบคุมไปให้ 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 แบบเขียนเองไม่ทำให้เลยสักอย่าง

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 yield
print(gen.send("hello")) # HELLO
print(gen.send("world")) # WORLD

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() บน 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 1
gen.close() # releasing resource

generator ที่ถูก garbage collect ก่อนจะถูกใช้จนหมด ก็จะถูกเรียก .close() ให้อัตโนมัติเช่นกัน ดังนั้น block finally ก็ยังทำงานอยู่ — แต่การพึ่งจังหวะของ garbage collection สำหรับ cleanup นั้นเปราะบาง ให้เรียก .close() เอง (หรือขับ generator ผ่าน with contextlib.closing(gen):) เมื่อรู้ล่วงหน้าว่าจะจบก่อนกำหนด

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 ไม่ 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.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]

เช่นเดียวกับที่ list comprehension สร้าง list generator expression ก็สร้าง generator โดยใช้วงเล็บแทนวงเล็บเหลี่ยม วิธีนี้ประหยัด memory เพราะไม่มีการสร้าง list ขึ้นมาจริง

squares_list = [x ** 2 for x in range(1_000_000)] # allocates a list
squares_gen = (x ** 2 for x in range(1_000_000)) # allocates almost nothing
# Consume lazily
for sq in squares_gen:
if sq > 25:
break

generator expression เข้ากันได้อย่างเป็นธรรมชาติกับ built-in ที่รับ iterable:

total = sum(x ** 2 for x in range(10)) # no brackets needed inside sum()

เนื่องจาก 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 early
first_10 = list(islice(fibonacci(), 10))
print("First 10 Fibonacci numbers:")
print(first_10)
# Generator expression: squares of even numbers
even_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 nature
gen = (x * 3 for x in range(5))
print("\nFirst pass:", list(gen))
print("Second pass:", list(gen)) # exhausted — empty list
# Manual next() / StopIteration
counter = 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")
import sys
from itertools import islice, chain
from 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 None
print(gen2.send("hello")) # HELLO
print(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)

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: Created -> Suspended (วนที่ yield) -> Returned หรือ Closed
สิ่งที่ได้ประโยชน์ต้นทุน
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 โดยไม่ต้อง materializeconsume generator ต้นทาง — “ย้อนกลับ” ผ่านสิ่งที่ islice ดึงไปแล้วไม่ได้
  • “iterate generator ที่หมดแล้วจะ raise error” — ข้อเท็จจริง: มันผลิตค่าเป็นศูนย์ตัวแบบเงียบ ๆ ไม่มี exception ใด ๆ — ไม่มี StopIteration รั่วออกมาจาก for loop หรือ list comprehension เลยไม่ว่ากรณีไหน จึงสังเกตพลาดได้ง่ายตอน review
  • “yield from inner() เหมือนกับ for x in inner(): yield x” — ข้อเท็จจริง: yield from ยังส่งต่อ .send()/.throw() เข้าไปใน inner() และประเมินค่าเป็น return value ของ inner() ด้วย ในขณะที่ loop แบบเขียนเองไม่ทำอย่างใดอย่างหนึ่งเลย
  • “return value ของ generator คือสิ่งที่ for loop ได้รับตอนจบ” — ข้อเท็จจริง: for loop ไม่มีวันเห็นค่านี้เลย — มันถูกแนบไว้ที่ StopIteration.value ดึงออกมาได้แค่ผ่านการ iterate ด้วยมือหรือ yield from เท่านั้น
  • “.close() แค่หยุด generator โดยไม่มี cleanup” — ข้อเท็จจริง: มัน raise GeneratorExit ตรงจุดที่หยุดชั่วคราวอยู่ ดังนั้น try/finally รอบมันก็ยังทำงาน เหมือนการปิด context manager เป๊ะ ๆ
  • “generator expression ก็เหมือน list แค่เขียนด้วย ()” — ข้อเท็จจริง: sys.getsizeof แสดงว่าขนาดของ generator คงที่ (~200 byte) ไม่ว่า range จะใหญ่แค่ไหน ในขณะที่ขนาดของ list โตตามจำนวน element — generator ยังไม่ได้คำนวณหรือเก็บค่าไหนไว้เลย
  • 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 ดูเหมือนตั้งใจให้ไปถึง for loop หรือเปล่า? มันไปถึงแค่ StopIteration.value เท่านั้น — for loop ธรรมดาจะทิ้งมันไปแบบเงียบ ๆ
  • resource (file, lock, connection) ถูกเปิดก่อน yield โดยไม่มี try/finally หรือเปล่า? ถ้า generator ถูกทิ้งกลางทาง .close() (หรือ garbage collection) ก็ยัง raise GeneratorExit ที่ yield นั้น และถ้าไม่มี finally resource จะไม่ถูกปล่อยเลย
  • 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 นั้น

เกิดอะไรขึ้นเมื่อ Python เจอ yield ภายใน function?
โค้ดนี้พิมพ์อะไร? def words(): yield "a" yield "b" gen = words() first = list(gen) second = list(gen) print(first, second)
โค้ดนี้พิมพ์อะไร? def gen(): x = yield 1 y = yield x return y g = gen() print(next(g)) print(g.send("A")) try: g.send("B") except StopIteration as e: print(e.value)
ทำไม yield from inner() ถึงเป็นที่นิยมมากกว่า for x in inner(): yield x โดยทั่วไป?
generator หนึ่งมี block try: ... yield ... finally: cleanup() ถ้า generator ถูก garbage collect ก่อนจะถูก iterate จนจบ cleanup() จะเป็นอย่างไร?
sys.getsizeof แสดงอะไรเมื่อเทียบ [x for x in range(1_000_000)] กับ (x for x in range(1_000_000))?