On this page
Iterator & Generator (yield)
เข้าใจว่า for ทำงานเบื้องหลังอย่างไร และสร้างข้อมูลทีละชิ้นแบบประหยัด memory ด้วย generator
ทุกครั้งที่คุณเขียน for x in something เบื้องหลังมีกลไกชื่อ iterator ทำงานอยู่ การเข้าใจมันทำให้คุณสร้าง generator ได้ — เครื่องมือที่ผลิตข้อมูลทีละชิ้นเมื่อต้องการ ไม่ต้องเก็บทั้งหมดใน RAM ซึ่งสำคัญมากเวลาทำงานกับข้อมูลขนาดใหญ่
iterable vs iterator
iterable คือสิ่งที่ "วนได้" (list, str, dict, range) ส่วน iterator คือ "ตัววน" ที่จำได้ว่าตอนนี้อยู่ตำแหน่งไหน เรียก next() เพื่อขอตัวถัดไป
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 คือ "หยุดพักแล้วส่งค่าออกไป" รอบหน้ามาต่อจากจุดเดิม
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)) # 1generator = สายพานผลิตทีละชิ้นเมื่อขอ ส่วน list = ผลิตทุกชิ้นกองไว้ก่อน ถ้าต้องการเลข 1 ถึง 10 ล้าน list จะกิน RAM มหาศาล แต่ generator เก็บแค่ค่าปัจจุบันทีละตัว นี่คือเหตุผลที่ range() ไม่กิน RAM
generator expression
เหมือน list comprehension แต่ใช้วงเล็บ ( ) แทน [ ] ได้ generator แทน list — เหมาะเมื่อจะวนผ่านครั้งเดียวไม่ต้องเก็บ
# 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
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 จนหมดแล้ว มันจะว่างเปล่า วนซ้ำอีกครั้งจะไม่ได้อะไร ถ้าต้องใช้หลายรอบให้แปลงเป็น 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 จนหมดแล้ววนอีกรอบ สังเกตว่าได้อะไร