Notes & software courses · Free to learn
Aph's Blog
On this page

Iterator & Generator (yield)

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

เข้าใจว่า for ทำงานเบื้องหลังอย่างไร และสร้างข้อมูลทีละชิ้นแบบประหยัด memory ด้วย generator

ทุกครั้งที่คุณเขียน for x in something เบื้องหลังมีกลไกชื่อ iterator ทำงานอยู่ การเข้าใจมันทำให้คุณสร้าง generator ได้ — เครื่องมือที่ผลิตข้อมูลทีละชิ้นเมื่อต้องการ ไม่ต้องเก็บทั้งหมดใน RAM ซึ่งสำคัญมากเวลาทำงานกับข้อมูลขนาดใหญ่

iterable vs iterator

iterable คือสิ่งที่ "วนได้" (list, str, dict, range) ส่วน iterator คือ "ตัววน" ที่จำได้ว่าตอนนี้อยู่ตำแหน่งไหน เรียก next() เพื่อขอตัวถัดไป

python
nums = [10, 20, 30]       # list เป็น iterable
it = iter(nums)           # ขอ iterator ออกมา

print(next(it))   # 10
print(next(it))   # 20
print(next(it))   # 30
# next(it) อีกครั้งจะเกิด StopIteration (หมดแล้ว)

# ที่จริง for ทำแบบนี้ให้เราอัตโนมัติ:
for n in nums:
    print(n)

generator: ฟังก์ชันที่ใช้ yield

generator คือฟังก์ชันที่ใช้ yield แทน return เมื่อเรียกมันจะ "ไม่รันทันที" แต่คืน generator object ที่ผลิตค่าทีละตัวเมื่อถูกขอ — แต่ละ yield คือ "หยุดพักแล้วส่งค่าออกไป" รอบหน้ามาต่อจากจุดเดิม

python
def count_up_to(n):
    i = 1
    while i <= n:
        yield i        # ส่งค่าออก แล้วหยุดพักตรงนี้
        i += 1         # รอบถัดไปมาต่อจากบรรทัดนี้

for num in count_up_to(3):
    print(num)         # 1, 2, 3

# เรียกเฉย ๆ ยังไม่รัน — ได้ generator object
gen = count_up_to(3)
print(gen)             # <generator object ...>
print(next(gen))       # 1
ทำไมประหยัด memory

generator = สายพานผลิตทีละชิ้นเมื่อขอ ส่วน list = ผลิตทุกชิ้นกองไว้ก่อน ถ้าต้องการเลข 1 ถึง 10 ล้าน list จะกิน RAM มหาศาล แต่ generator เก็บแค่ค่าปัจจุบันทีละตัว นี่คือเหตุผลที่ range() ไม่กิน RAM

generator expression

เหมือน list comprehension แต่ใช้วงเล็บ ( ) แทน [ ] ได้ generator แทน list — เหมาะเมื่อจะวนผ่านครั้งเดียวไม่ต้องเก็บ

python
# list comprehension: สร้าง list จริง กิน memory
squares_list = [x * x for x in range(1000000)]

# generator expression: ไม่สร้าง list ผลิตทีละตัว
squares_gen = (x * x for x in range(1000000))

# ใช้กับ sum() ได้เลย ไม่ต้องสร้าง list กลาง
total = sum(x * x for x in range(10))
print(total)   # 285

อ่านไฟล์ใหญ่ทีละบรรทัด

ตัวอย่างจริงที่ generator เปล่งประกาย: อ่านไฟล์ขนาดหลาย GB โดยไม่โหลดทั้งไฟล์เข้า RAM

python
def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:          # f เป็น iterator อยู่แล้ว
            yield line.strip()

# วนทีละบรรทัด ไม่ว่าไฟล์ใหญ่แค่ไหนก็ไม่ล้น RAM
# for line in read_lines("huge.txt"):
#     process(line)
generator ใช้ได้ครั้งเดียว

เมื่อวน generator จนหมดแล้ว มันจะว่างเปล่า วนซ้ำอีกครั้งจะไม่ได้อะไร ถ้าต้องใช้หลายรอบให้แปลงเป็น list ด้วย list(gen) หรือสร้าง generator ใหม่

สรุปหัวข้อนี้

  • iterable = วนได้, iterator = ตัววนที่จำตำแหน่ง (เรียกด้วย iter()/next())
  • generator = ฟังก์ชันที่ใช้ yield ผลิตค่าทีละตัว ประหยัด memory
  • generator expression = (x for x in ...) ใช้แทน list เมื่อวนครั้งเดียว
  • generator ใช้ได้ครั้งเดียว — หมดแล้วต้องสร้างใหม่
แบบฝึกหัด

1) เขียน generator ที่ให้เลขฟีโบนักชี n ตัวแรก 2) เขียน generator expression หาผลรวมของเลขคี่ 1-100 3) เขียน generator ที่อ่านไฟล์แล้ว yield เฉพาะบรรทัดที่มีคำว่า "error" 4) ลองวน generator จนหมดแล้ววนอีกรอบ สังเกตว่าได้อะไร