Nattawut Phetmak
Jack of all Trades
โดยปรกติแล้ว ในฟังก์ชันหนึ่งๆ จะจบการทำงานทันทีที่เจอ return
ตัวแรก นั่นคือการคืนค่าเพียง 1 เดียวของมัน
โดยถ้าเราไม่ต้องการให้ฟังก์ชันของเราจบการทำงานทันที แต่ก็ยังต้องการผลลัพท์ระหว่างการทำงานเป็นรอบๆ ออกมาเรื่อยๆ นี่คือแนวคิดของ generator นั่นเอง ซึ่งสามารถสร้างได้ง่ายๆ โดยการเปลี่ยน keyword จาก return
เป็น yield
เช่นตัวอย่างนี้
def gen_prime():
yield 2
yield 3
yield 5
yield 7
ในการใช้งาน generator นั้น ปรกติเราจะเรียกมันผ่าน iterator ทั้งหลายอยู่แล้ว เช่น
for p in gen_prime():
print(p**2)
แต่ถ้าต้องการเรียก generator เป็น object ตรงๆ เพื่อควบคุมการดึงค่าออกมาให้ได้มากขึ้น ก็สามารถทำได้โดย
gen_obj = gen_prime()
print(next(gen_obj))
# get: 2
print(next(gen_obj))
# get: 3
print(next(gen_obj))
# get: 5
print(next(gen_obj))
# get: 7
print(next(gen_obj))
# get exception: StopIteration
ข้อดีของมันเหนือการสร้างฟังก์ชันที่วิ่งวนหาค่าที่ต้องการทุกตัวแล้วส่งคืนมาเป็น list คือ generator จะทำงานเท่าที่ร้องขอเท่านั้น lazy evaluation ในรอบการทำงานหนึ่งๆ เมื่อเจอ keyword yield
ก็จะหยุดพัก จนกว่าจะถูกเรียกอีกโดย next()
มันถึงจะวิ่งไปหา yield
ตัวถัดไป ทำให้ประหยัดหน่วยความจำมากกว่าการเตรียมทุกค่าไว้ก่อน แถมยังประหยัดเวลาอีกด้วยในกรณีที่มีการ break
ออกจาก iterator
และจะยิ่งเห็นข้อดีเด่นชัดขึ้นอีกอย่าง เมื่อเราสามารถนิยาม generator ให้อยู่ในรูปของ iterator ได้ เช่นจากตัวอย่างข้างบน เขียนใหม่สำหรับกรณีทั่วไปได้ดังนี้
def gen_prime(stop=0):
n = 2
while n < stop or not stop:
for i in range(2, n):
if n%i == 0:
break
else:
yield n
n += 1
ข้อเสียของมันคงเป็นการที่ไม่สามารถระบุตำแหน่งของข้อมูลได้ เช่นตามตัวอย่างนี้ เราไม่สามารถบอกได้ว่าจำนวนเฉพาะตัวที่ 15 คืออะไร จนกว่าเราจะแปลง generator ไปอยู่ในรูปของข้อมูลแบบอื่นๆ อย่างเช่น list ครับ