ในบทนี้จะพูดถึงเรื่องของ
อิเทอเรเตอร์ (iterator) ซึ่งเป็นออบเจ็กต์พิเศษชนิดหนึ่งในภาษาไพธอน สามารถนำมาใช้ประโยชน์ได้ในหลายทาง แม้ว่าอาจไม่ถึงกับจำเป็นเพราะมีสิ่งอื่นๆที่สามารถใช้แทนได้
ใน
บทที่ ๑๒ได้เกริ่นไปนิดหน่อยว่าฟังก์ชัน
reversed
ทำให้เกิดสิ่งที่เรียกว่าอิเทอเรเตอร์ และใน
บทที่ ๒๑ พูดถึงฟังก์ชัน
map
และ
filter
ซึ่งก็ทำให้เกิดอิเทอเรเตอร์เช่นกัน แต่ไม่ได้อธิบายขยายความว่าอิเทอเรเตอร์คืออะไร ดังนั้นจึงจะมาพูดถึงในบทนี้
เรื่องที่จะอธิบายในบทนี้เป็นสิ่งที่ ค่อนข้างเข้าใจยาก และอาจจะไม่ถึงกับมีความจำเป็น อย่างไรก็ตามก็ถือเป็นการปูพื้นฐานเพิ่มเติมเพื่อความเข้าใจถึงระบบของภาษา นี้ในระดับที่ลึกลงไปมากยิ่งขึ้น
อิเทอเรเตอร์เป็นแนวคิดที่สำคัญมาก อย่างหนึ่งในภาษาไพธอน เพราะเป็นหลักการทำงานของวังวน
for
ซึ่งต้องใช้งานเป็นประจำ แม้ว่าโดยทั่วไปผู้เรียนเริ่มต้นอาจยังไม่มีความจำเป็นต้องเข้าใจว่า
for
เป็นการทำงานของอิเทอเรเตอร์ก็ตาม
อิเทอเรเตอร์คืออะไร คำว่า
อิเทอเรเตอร์ (iterator) นั้นมาจากคำว่า iterate ที่แปลว่าการทำซ้ำ ดังนั้นหากแปลตรงตัวแล้วอิเทอเรเตอร์ก็คือ "ตัววนซ้ำ" ดังนั้นจากชื่อก็คงพอจะเดาได้แล้วว่ามันคืออะไรบางอย่างที่มีการทำงาน เป็นการวนทำอะไรซ้ำๆนั่นเอง
อิเทอเรเตอร์เป็นออบเจ็กต์ชนิดหนึ่งซึ่งทำหน้าที่วนซ้ำเพื่อดำเนินการบางอย่างและส่งข้อมูลคืนกลับออกมาเรื่อยๆในแต่ละครั้งที่วน
อธิบายด้วยคำพูดแบบนี้คงจะยากที่จะเข้าใจได้ทันที ดังนั้นจะขอเริ่มด้วยการยกตัวอย่างการเปรียบเทียบว่าอิเทอเรเตอร์นั้นก็ เสมือนกับออบเจ็กต์ชนิดลำดับ เช่น ลิสต์ หรือ ทูเพิล
ตัวอย่างที่เราจะเห็นการใช้งานอิเทอเรเตอร์ที่ชัดที่สุดก็คือเมื่อใช้คำสั่ง
for
เพื่อวนซ้ำ
โดยทั่วไปแล้ว for จะใช้คู่กับข้อมูลชนิดกลุ่ม เช่น ลิสต์ ตัวอย่างเช่น
lis = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
for i in lis:
print(i)
จะเห็นว่าหากต้องการให้วนซ้ำกี่รอบก็ต้องเตรียมลิสต์ที่มีจำนวนสมาชิกตามที่ต้องการ
ในตัวอย่างนี้มีแค่สามสิบตัวอาจยังดูไม่เยอะเท่าไหร่ แต่หากต้องการให้วนซ้ำเป็นพันเป็นหมื่นรอบก็จะต้องมีลิสต์ที่มีสมาชิกมากมาย ตามนั้น
ซึ่งก็มีข้อเสียอยู่ เช่น
- ข้อมูลจำนวนมากก็ต้องการพื้นที่เก็บจำนวนมาก กินที่ในหน่วยความจำ
- อีกทั้งยังต้องใช้เวลาในการสร้างข้อมูลจำนวนมากเพื่อทำเป็นลิสต์ ทำให้เสียเวลากว่าจะได้เริ่มดำเนินการ
- และการวนซ้ำใน
for
นั้นก็ไม่ใช่ว่าจำเป็นจะต้องวนจนครบทุกรอบเสมอไป บางครั้งอาจหลุดออกมาก่อน เช่นเจอคำสั่ง break
กลางคันเมื่อเข้าเงื่อนไขอะไรบางอย่าง
ดังนั้นจึงเกิดความคิดว่าหาก มีอะไรบางอย่างที่สามารถค่อยๆสร้างข้อมูลออกมาได้ทีละนิดเมื่อจะมีการใช้งาน แบบนั้นก็จะไม่เสียเวลาในการสร้างข้อมูลจำนวนมาก และพื้นที่ในการเก็บข้อมูลด้วย
สิ่งนั้นก็คืออิเทอเรเตอร์นั่นเอง
ลำดับของข้อมูลอะไรบางอย่างที่มีรูปแบบตายตัวสามารถทำเป็นอิเทอเรเตอร์ได้ เช่นชุดตัวเลขที่เป็นลำดับอนุกรมที่ใช้สูตรคำนวนหรือความสัมพันธ์เวียนเกิดได้
เรนจ์จัดเป็นอิเทอเรเตอร์ชนิดหนึ่ง ดังที่ได้อธิบายไปใน
บทที่ ๘ เรนจ์ไม่ได้เก็บข้อมูลตัวเลขจำนวนมากไว้ แต่เก็บเฉพาะพารามิเตอร์ ๓ ตัวคือจุดเริ่ม, จุดสิ้นสุด และระยะเว้น แล้วเมื่อมีการเรียกใช้จึงค่อยๆสร้างตัวเลขออกมาทีละตัว
นอกจากนี้ยังมีอีกหลายอย่างที่มีคุณสมบัติเป็นอิเทอเรเตอร์ ในบทที่ผ่านๆมาก็ได้เห็นกันไปแล้วบ้างแต่ไม่ได้ลงรายละเอียด
อิเทอเรเตอร์มีหลากหลายรูปแบบ มีหลายวิธีในการสร้างอิเทอเรเตอร์ขึ้นมา
โดยพื้นฐานแล้วอิเทอเรเตอร์สร้างขึ้นมาจากการนิยามคลาสในรูปแบบที่ต้องการให้เป็น อย่างไรก็ตามการสร้างคลาสอิเทอเรเตอร์นั้นเป็นเรื่องที่ค่อนข้างเข้าใจยาก
ดังนั้นจะขอเริ่มพูดถึงจากอิเทอเรเตอร์อย่างง่ายที่เรียกว่า
เจเนอเรเตอร์ (generator) ฟังก์ชันสร้างเจเนอเรเตอร์ เจเนอเรเตอร์ (generator) เป็นรูปแบบหนึ่งของอิเทอเรเตอร์ เป็นอิเทอเรเตอร์ที่ถูกสร้างขึ้นมาอย่างง่ายจากการใช้ฟังก์ชันสร้าง
การสร้างเจเนอเรเตอร์ขึ้นมานั้นทำได้โดยการนิยามฟังก์ชันด้วยคำสั่ง
def
เหมือนกับฟังก์ชันธรรมดาทั่วไป
โดยทั่วไปแล้วฟังก์ชันทั่วไปจะใช้คำสั่ง
return
เพื่อส่งค่าคืนกลับ แต่หากต้องการสร้างเจเนอเรเตอร์ขึ้นจะใช้คำสั่ง
yield
แทน
return
เพียงแค่มีคำว่า
yield
อยู่ในฟังก์ชัน ฟังก์ชันนั้นก็จะกลายเป็นฟังก์ชันสร้างเจเนอเรเตอร์ทันที และคุณสมบัติการทำงานจะต่างไปจากฟังก์ชันทั่วไปทันที
โดยทั่วไปแล้ว ฟังก์ชันทั่วไปนั้นเมื่อเริ่มทำงานจะเริ่มอ่านโค้ดจากหัวไปท้ายถ้าเจอ
return
ที่ไหนฟังก์ชันก็จะสิ้นสุดการทำงานทันทีพร้อมทั้งส่งค่าคืนกลับมา จากนั้นเมื่อฟังก์ชันนี้ถูกเรียกซ้ำก็จะเริ่มทำงานใหม่ตั้งแต่ต้น ไม่ได้มีอะไรเกี่ยวข้องกับรอบที่แล้ว
แต่ฟังก์ชันที่มี
yield
นั้นการทำงานจะต่างออกไป คือเมื่อถูกเรียกใช้งานมันจะยังไม่ได้เริ่มทำงาน แต่จะแค่เริ่มทำการสร้างเจเนอเรเตอร์ขึ้นมา
และเจเนอเรเตอร์นี้จะถูกเรียกใช้งานเมื่อเจอคำสั่ง
for
in
โดยเจเนอเรเตอร์นี้จะถูกใช้แทนข้อมูลชนิดกลุ่ม
การทำงานของฟังก์ชันนี้จะเริ่มอ่านโค้ดจากเริ่มต้นฟังก์ชันและจะหยุดทำงานเมื่อ เจอ
yield
และส่งค่าคืนกลับให้กับตัวแปรที่มารับค่าในคำสั่ง for
ในจังหวะนี้ฟังก์ชันนี้จะหยุดทำงาน แต่ก็เป็นการหยุดเพียงชั่วคราว จากนั้นเมื่อวนมาถึงรอบต่อไปฟังก์ชันจะทำงานต่อ โดยเริ่มอ่านโค้ดต่อจาก
yield
ตัวนั้น และต่อไปสุดที่
yield
ตัวต่อไป แล้วก็ส่งค่าคืนกลับมาสำหรับใช้ในรอบต่อไป
ทำแบบนี้ไปเรื่อยๆจนกว่าจะไม่เจอ
yield
อีกต่อไปแล้วฟังก์ชันนั้นจึงหยุดทำงาน และวังวน
for
ก็จะสิ้นสุดลงเท่านี้
อธิบายด้วยคำพูดอาจเข้าใจยาก มาดูตัวอย่างน่าจะช่วยให้เห็นภาพชัดขึ้น
def gensoukyou():
yield 'gen' # ส่งค่าคืนกลับครั้งที่ ๑
yield 'sou' # ส่งค่าคืนกลับครั้งที่ ๒
yield 'kyou' # ส่งค่าคืนกลับครั้งที่ ๓
gensou = gensoukyou() # สร้างเจเนอเรเตอร์ขึ้นจากฟังก์ชัน
for g in gensou: # ใช้เจเนอเรเตอร์แทนตัวแปรชนิดกลุ่มในคำสั่ง for
print(g)
ผลลัพธ์
gen
sou
kyou
ในตัวอย่างนี้ฟังก์ช้น
gensoukyou
สร้างเจเนอเรเตอร์ชื่อ
gensou
จากนั้น
gensou
ก็ถูกนำไปใช้ในคำสั่ง
for
โดยคืนค่าที่อยู่ข้างหลัง
yield
มาทีละค่าตามลำดับ
อนึ่ง จริงๆแล้วการสร้างตัวแปร
gensou
เพื่อมาเก็บเจเนอเรเตอร์นั้นอาจไม่จำเป็นก็ได้ อาจเขียนเป็น
for g in gensoukyou():
print(g)
นั่นคือนำตัวฟังก์ชันมาใช้แทนข้อมูลชนิดกลุ่มได้เลยโดยตรง ก็เหมือนกับเวลาที่ใช้ลิสต์ในคำสั่ง
for
เราอาจประกาศสร้างลิสต์ขึ้นมาก่อนแล้วค่อยเอาตัวแปรที่เก็บลิสต์นั้นมาใช้ หรือจะสร้างลิสต์ขึ้นตรงนั้นเพื่อใช้ทันทีเลยก็ได้
อย่างไรก็ตามอาจต้องย้ำสักหน่อยว่าตัวฟังก์ชันที่สร้างเจเนอเรเตอร์เองนั้นไม่ใช่ตัวเจเนอเรเตอร์ แต่มีสถานะเป็นฟังก์ชัน แต่ผลที่ได้จากฟังก์ชันนั้นจึงจะเป็นตัวเจเนอเรเตอร์ ลองทดสอบโดยใช้
type
ดูได้
print(type(gensoukyou)) # ได้ <class 'function'>
print(type(gensoukyou())) # ได้ <class 'generator'>
gensou = gensoukyou()
print(type(gensou)) # ได้ <class 'generator'>
ฟังก์ชันแบบนี้สามารถรับค่าตัวแปรมาใช้ได้เช่นเดียวกับฟังก์ชันทั่วไป
def genjitsu(a,b,c):
yield a
yield b
yield c
for g in genjitsu('dai','su','ki'):
print(g)
ผลลัพธ์
dai
su
ki
โดยทั่วไปแล้วแทนที่จะเขียน
yield
หลายๆตัวแบบนี้ การใช้
yield
มักจะใช้คู่กับการวนซ้ำด้วย
while
หรือ
for
มากกว่า เช่น
def genkotsu(n):
i = 0
while(i<n):
i += 1
yield i
for g in genkotsu(10):
print(g, end=' ')
ผลลัพธ์
1 2 3 4 5 6 7 8 9 10
ในตัวอย่างนี้จะเกิดการวนซ้ำด้วย
while
ตามจำนวนครั้งที่ป้อนเข้าไป ในที่นี้คือ
10
โดยในแต่ละรอบจะเจอ
yield
และคืนค่ากลับมาทุกครั้งและ
i
ก็มากขึ้นเรื่อยๆ จนเมื่อ
i
เท่ากับ
10
ก็จะออกจากวังวนไม่เจอ
yield
อีก การทำงานของฟังก์ชันจึงสิ้นสุดลง
จะสังเกตได้ว่าตัวแปร
i
ซึ่งประกาศขึ้นภายในฟังก์ชันนี้มีการเปลี่ยนแปลงค่าไปเรื่อยๆทุกครั้ง ไม่ได้กลับมาเริ่มต้นใหม่ทุกครั้งที่ฟังก์ชันหยุดทำงานชั่วคราวหลังเจอ
yield
ซึ่งต่างจากฟังก์ชันทั่วไปที่ใช้
return
ซึ่งเมื่อสิ้นสุดการทำงานตัวแปรจะต้องถูกลบทิ้งไป
ลองดูเจเนอเรเตอร์ที่มีการทำงานเหมือนฟังก์ชัน
reversed
คือกลับลำดับของข้อมูลกลุ่ม
def genreversed(c):
i = len(c)-1
while(i>=0):
yield c[i]
i -= 1
print([x for x in genreversed(range(3,9))]) # ได้ [8, 7, 6, 5, 4, 3]
ลองดูตัวอย่างที่ซับซ้อนขึ้นไปอีก เช่นแฟกทอเรียล
def genfac(n):
i = 2
fac = 1
while(i<=n+1):
yield fac
fac *= i
i += 1
for g in genfac(7):
print(g, end='~')
ผลลัพธ์
1~2~6~24~120~720~5040~
หรือฟีโบนัชชี
def genfib(n):
i = 1
a = 0
b = 1
while(i<=n):
yield b
a,b = b,a+b
i += 1
for g in genfib(7):
print(g, end='::')
ผลลัพธ์
1::1::2::3::5::8::13::
ทั้งสองฟังก์ชันนี้ได้มีการยกตัวอย่างไว้ใน
บทที่ ๒๐ แล้ว โดยใช้ฟังก์ชันแบบธรรมดาที่มีการใช้คำสั่ง
return
สามารถลองไปดูเพื่อเปรียบเทียบกันได้
หากจะเขียนฟีโบนัชชีด้วยฟังก์ชันธรรมดาให้ได้ผลแบบเดียวกันจะต้องเขียนแบบนี้
def fib(x):
i = 2
a = 1
b = 1
while(i<x):
a,b = b,a+b
i += 1
return b
for i in range(1,8):
print(fib(i), end='::')
เทียบกันแล้วจะเห็นว่าในกรณีที่ต้องการได้ตัวเลขไล่มาทีละตัวเป็นลำดับทั้งหมดแบบนี้การใช้เจเนอเรเตอร์จะสะดวกกว่า เพราะเดินหน้าอ่านโค้ดเพื่อสร้างตัวเลขออกมาเรื่อยๆตามลำดับอย่างต่อเนื่อง โดยที่ไม่ต้องมาเริ่มต้นใหม่
ในขณะที่หากเป็นฟังก์ชันธรรมดาการจะได้สมาชิกลำดับหลังๆจะต้องผ่านการคำนวณตัวเลขในลำดับก่อนๆเพื่อเอาแค่ค่า สุดท้ายมาส่งเป็นค่าคืนกลับ ดังนั้นทุกครั้งที่เรียกใช้ฟังก์ชันจะต้องมีการคำนวณซ้ำใหม่ทุกครั้งไล่ไปจนถึงลำดับที่ต้องการ
การใช้งานอื่นๆของอิเทอเรเตอร์และเจเนอเรเตอร์ นอกจากการใช้งานในการวนซ้ำด้วย
for
แล้ว บางอย่างที่ออบเจ็กต์ชนิดกลุ่มทำได้นั้นอิเทอเรเตอร์ก็สามารถทำได้เช่นกัน
***หัวข้อนี้ใช้เจเนอเรเตอร์เป็นตัวอย่าง แต่เจเนอเรเตอร์เป็นอิเทอเรเตอร์ชนิดหนึ่งและคุณสมบัติเหล่านี้ใช้กับอิเทอเรเตอร์ที่ไม่ใช่เจเนอเรเตอร์ได้ด้วย
ตัวอย่างเช่น การนำมาใช้ในการสร้างลิสต์ด้วย
for
print([x*10 for x in genfib(7)]) # ได้ [10, 10, 20, 30, 50, 80, 130]
หรือสามารถนำมาแปลงเป็นลิสต์หรือทูเพิลหรือเซ็ตได้
print(list(genfib(7))) # ได้ [1, 1, 2, 3, 5, 8, 13]
print(tuple(genfib(7))) # ได้ (1, 1, 2, 3, 5, 8, 13)
print(set(genfib(7))) # ได้ {1, 2, 3, 5, 8, 13}
บางฟังก์ชันที่ปกติใช้กับออบเจ็กต์ชนิดกลุ่มเช่น
any
,
all
,
map
และ
filter
ก็ยังใช้กับอิเทอเรเตอร์ได้
print(list(map(str,genfib(7)))) # ได้ ['1', '1', '2', '3', '5', '8', '13']
print(list(filter(lambda x:x%2,genfib(7)))) # ได้ [1, 1, 3, 5, 13]
print(all(genfib(7))) # ได้ True
และยังใช้กับ
in
เพื่อตรวจสอบว่ามีออบเจ็กต์ที่ต้องการอยู่ในกลุ่มหรือเปล่าได้ด้วย
print(13 in genfib(7)) # ได้ True
print(9 in genfib(9)) # ได้ False
สำหรับอิเทอเรเตอร์ที่คืนค่าสายอักขระยังสามารถใช้กับเมธอด
.join
ได้ด้วย เช่น
def genjimonogatari():
yield 'เกน'
yield 'จิ'
yield 'โม'
yield 'โน'
yield 'งา'
yield 'ตา'
yield 'ริ'
print('-'.join(genjimonogatari())) # ได้ เกน-จิ-โม-โน-งา-ตา-ริ
ฟังก์ชัน next ที่เราเห็นว่าเจเนอเรเตอร์ทำงานเมื่อใช้คำสั่ง
for
นั้น แท้จริงแล้วมีกลไกที่อยู่เบื้องหลัง นั่นคือความจริงแล้วคำสั่ง
for
เป็นการสั่งให้เจเนอเรเตอร์ใช้ฟังก์ชัน
next
ซ้ำๆอย่างต่อเนื่อง
ฟังก์ชัน
next
เป็นคำสั่งสำคัญสำหรับอิเทอเรเตอร์และเจเนอเรเตอร์ มีหน้าที่สั่งให้เจเนอเรเตอร์เริ่มทำงานโดยในการใช้ครั้งแรกจะเริ่มอ่านโค้ด จากเริ่มต้นไปจนกว่าจะเจอคำสั่ง
yield
แล้วก็ส่งค่าคืนกลับ แล้วฟังก์ชันก็จะหยุดทำงาน และจะทำงานต่อเมื่อใช้ฟังก์ชัน
next
อีกครั้ง
ข้อยกตัวอย่างโดยใช้ฟังก์ชัน
genfib
ที่นิยามไว้ในตัวอย่างก่อนหน้านี้
genji = genfib(5)
print(next(genji)) # ได้ 1
print(next(genji)) # ได้ 1
print(next(genji)) # ได้ 2
print(next(genji)) # ได้ 3
print(next(genji)) # ได้ 5
จะเห็นว่าเมื่อใช้
next
กับเจเนอเรเตอร์แล้วจะเป็นการสั่งให้มันคืนค่าออกมาทีละตัวเช่นเดียวกับเวลาที่ใช้
for
แท้จริงแล้วเวลาที่ใช้
for
กับเจเนอเรเตอร์หรืออิเทอเรเตอร์นั้น ก็คือการเรียกใช้
next
ไปเรื่อยๆจนกว่าจะใช้ไม่ได้แล้วนั่นเอง
ฟังก์ชัน
genfib
ที่เราทำไว้นี้จะให้ตัวเลขออกมาตามลำดับเป็นจำนวนเท่ากับเลขที่ใส่ลงไป ในตัวอย่างนี้เราใส่ไป 5 ดังนั้นจึงสามารถใช้คำสั่ง
next
ได้ ๕ ครั้ง
จากนั้นเมื่อใช้
next
ครั้งต่อไปโค้ดจะวิ่งต่อไปจนสุดฟังก์ชันโดยที่หา
yield
ตัวต่อไปไม่เจอ เมื่อเป็นแบบนี้โปรแกรมจะเกิดข้อผิดพลาดขึ้นมา
ลองใช้
next
ต่อจากตัวอย่างนี้
print(next(genji)) # ได้ StopIteration
เมื่อลองแล้วก็จะพบว่ามีข้อผิดพลาดปรากฏขึ้นมาตามคาด ข้อผิดพลาดที่ปรากฏขึ้นนั้นมีชื่อว่า
StopIteration
ซึ่งมีความหมายว่าสิ้นสุดการทำงานของอิเทอเรเตอร์หรือเจเนอเรเตอร์นั้นแล้ว
ในกรณีของเจเนอเรเตอร์ที่สร้างขึ้นจาก
def
และ
yield
นั้น
StopIteration
จะเกิดขึ้นเมื่อฟังก์ชันถูกอ่านไปจนถึงสุดโดยที่ไม่เจอคำสั่ง
yield
อีกแล้ว
โดยทั่วไปแล้วเวลาที่ใช้
for
เมื่อมีการวนซ้ำมาจนหา
yield
ไม่เจอ สิ่งที่จะเกิดขึ้นก็คือหยุดการวนซ้ำโดยที่ไม่เกิดข้อผิดพลาดขึ้น
แต่ความจริงแล้ว
StopIteration
ก็เกิดขึ้นเวลาที่ใช้
for
เช่นกัน เพียงแต่โปรแกรมจะตอบสนองด้วยการปล่อยให้ผ่านไปแล้วตัดออกจากวังวนแทน เหมือนกับการใช้
try
และ
except
กล่าวโดยรวมแล้วการใช้
for
อาจมีความหมายเท่ากับการเขียนโค้ดในลักษณะเช่นนี้
genjisama = genfib(7)
while(1):
try:
print(next(genjisama), end='::')
except StopIteration:
break
นั่นคือการวนทำซ้ำแบบไม่มีเงื่อนไขจนกว่าจะ
next
ไปจนสุดแล้วเกิด
StopIteration
จึงหลุดออกไปด้วยคำสั่ง
break
เจเนอเรเตอร์ที่ผ่านการวนซ้ำด้วย
for
จนจบแล้วหากใช้
next
ต่อก็จะเกิดข้อผิดพลาดขึ้นเช่นกัน
genjisan = genfib(5)
for g in genji:
print(g)
print(next(genjisan)) # ได้ StopIteration
แต่หากใช้
for
ซ้ำกับเจเนอเรเตอร์ตัวเดิมหลังจากที่ใช้
for
ไปแล้วครั้งหนึ่งจะพบว่า
for
ตัวหลังไม่ทำงาน เพียงแต่จะไม่เกิดข้อผิดพลาดขึ้น
genjikun = genfib(5)
for g in genjikun:
print(g)
for h in genjikun: # ไม่ทำงาน
print(h)
สรุปก็คือเจเนอเรเตอร์ที่ถูกใช้งานจนจบแล้วจะไม่สามารถใช้งานต่อได้อีก ตรงนี้เป็นข้อแตกต่างจากออบเจ็กต์ชนิดกลุ่มซึ่งสามารถนำมาใช้กับคำสั่ง
for
กี่ครั้งก็ได้ เพราะจะกลับมาเริ่มไล่ใหม่ตั้งแต่ต้นทุกครั้งที่เริ่มใช้
for
และจะเห็นได้ว่าเมื่อใช้เจเนอเรเตอร์ใน
for
ไม่จำเป็นว่ามันจะถูกเริ่มตั้งแต่ต้นเสมอ ถ้าหากว่ามันมีการใช้
next
มาแล้วก่อนหน้านั้นก็จะเริ่มไล่ต่อจากตรงนั้นต่อเลย
genjichan = genfib(5)
print(next(genjichan)) # ครั้งที่ 1
for h in genjichan: # ครั้งที่ 2,3,4,5
print(h)
เจเนอเรเตอร์อนันต์ ข้อแตกต่างอีกอย่างหนึ่งระหว่างอิเทอเรเตอร์กับลิสต์ก็คือ ลิสต์มีจำนวนข้อมูลได้จำกัด แต่อิเทอเรเตอร์นั้นข้อมูลค่อยๆถูกสร้างขึ้นมาเรื่อยๆและอาจมีได้ไม่จำกัด แล้วแต่ว่าต้องการมากแค่ไหน
ตัวอย่างเช่น เจเนอเรอเตอร์อันนี้
def mugen(x):
i = 1
while(1): # วนซ้ำแบบไม่มีเงื่อนไข
yield x*i
i += 1
เจเนอเรเตอร์ที่เกิดจากฟังก์ชันนี้จะวนซ้ำด้วย
while
อย่างไม่มีที่สิ้นสุด อยากเรียกใช้กี่ครั้งก็ตามที่ต้องการ จะใช้
next
กี่ครั้งก็ยังสามารถใช้ได้อีกเรื่อยๆ
mu = mugen('ก')
for i in range(5):
print(next(mu), end='=')
ได้
ก=กก=กกก=กกกก=กกกกก=
แต่ต้องระวังว่าจะใช้
for
กับอิเทอเรเตอร์แบบนี้ไม่ได้ ถ้าไม่มีเงื่อนไขสำหรับให้หยุดกลางคัน เพราะจะเกิดวังวนที่ไม่มีที่สิ้นสุด
ดังนั้นจึงอาจต้องใช้
break
ช่วย เช่น
for mu in mugen('ม'):
print(mu, end='=')
if(len(mu)>7): break
ได้
ม=มม=มมม=มมมม=มมมมม=มมมมมม=มมมมมมม=มมมมมมมม=
นอกจากนี้ยังไม่สามารถแปลงเป็นลิสต์, ทูเพิล หรือเซ็ตได้ และจะใช้กับฟังก์ชันหรือเมธอดอย่าง
map
,
filter
หรือ
.join
ก็ไม่ได้เช่นกัน
การประยุกต์ใช้อาจลองใช้กับการนับจำนวน ลองยกตัวอย่างด้วยการใช้กับเกมตอบคำถามคล้ายๆกับที่ยกตัวอย่างใน
บทที่ ๗ แต่ลองเปลี่ยนมาใช้เจเนอเรเตอร์ในการนับแทน
def gennap():
n = 0
while(1):
n += 1
yield n
nap = gennap()
khamtop = int(input('ยานอวกาศลำแรกของโลกถูกส่งออกไปในปี ค.ศ. ใด: '))
while(khamtop!=1957):
if(khamtop>1957): print('เร็วกว่านั้น') # กรณีตอบเลขสูงไป
else: print('ช้ากว่านั้น') # กรณีตอบเลขต่ำไป
print('คุณตอบไปแล้ว %d ครั้ง'%next(nap))
khamtop = int(input('ตอบใหม่: '))
else: print('คำตอบถูกต้อง')
ในตัวอย่างนี้จะมีการวนซ้ำให้ตอบใหม่ไปเรื่อยๆตราบใดที่ตอบผิด โดยมีการใช้ฟังก์ชัน
next
ซึ่งจะคืนค่าที่สูงขึ้นเรื่อยๆทีละ
1
ในแต่ละรอบ การวนซ้ำจะสิ้นสุดลงเมื่อตอบคำถามถูกต้อง
การสร้างเจเนอเรเตอร์ขึ้นจาก for ในวงเล็บ โดยปกติแล้วเราสามารถสร้างลิสต์ขึ้นได้อย่างง่ายจาก
for
และวงเล็บเหลี่ยม อย่างที่กล่าวถึงตอนท้าย
บทที่ ๙
print([i**2 for i in range(7)]) # ได้ [0, 1, 4, 9, 16, 25, 36]
แต่หากใช้วงเล็บปีกกา
{ }
แทนที่จะใช้วงเล็บเหลี่ยม ก็จะได้เซ็ตแทน
print({i**2 for i in range(7)}) # ได้ {0, 1, 4, 36, 9, 16, 25}
และหากใช้วงเล็บโค้ง
( )
ก็จะได้ออกมาเป็นเจเนอเรเตอร์
genki = (i**2 for i in range(7))
print(genki) # ได้ at 0x112583938>
ซึ่งก็สามารถนำมาแปลงเป็นลิสต์หรือทูเพิลหรือเซ็ตได้อีกที
print(list(genki)) # ได้ [0, 1, 4, 9, 16, 25, 36]
นี่จึงเป็นอีกวิธีในการสร้างเจเนอเรเตอร์ขึ้นอย่างง่าย และความจริงแล้วในบางกรณีอาจดีกว่าการสร้างเป็นลิสต์ เพราะข้อมูลไม่ได้ถูกผลิตขึ้นมาทันที แต่แค่สร้างเจเนอเรเตอร์เพื่อรอสร้างข้อมูลเมื่อถูกเรียกใช้ ดังนั้นจะเร็วกว่ามาก
ยิ่งจำนวนสมาชิกมากยิ่งเห็นความแตกต่างชัด หากลองรันดูเทียบกัน จะเห็นความแตกต่างที่ชัดเจนมาก
(i**2 for i in range(10000000)) # สร้างเจเนอเรเตอร์ เสร็จในพริบตา
[i**2 for i in range(10000000)] # สร้างลิสต์ ต้องรอนาน
{i**2 for i in range(10000000)} # สร้างเซ็ต นานที่สุด
เพียงแต่ว่าหากสร้างขึ้นมาเพื่อใช้ใน
for
แล้ว เจเนอเรเตอร์จะมาเสียเวลาตอนที่ที่วนซ้ำเพราะต้องสร้างข้อมูลขึ้นมาใหม่ ทำให้เวลาที่ใช้ทั้งหมด (= เวลาเตรียมตัว + เวลาที่วนซ้ำ) โดยรวมแล้วเจเนอเรเตอร์จะเร็วกว่าไม่มาก
แต่หากว่าภายในการวนซ้ำนั้น มีเงื่อนไขบางอย่างที่ทำให้หลุดออกจากวังวน เช่นมี
break
แบบนี้เจเนอเรเตอร์จะประหยัดเวลากว่ามากเพราะเสียเวลาสร้างข้อมูลแค่เท่ากับ จำนวนครั้งที่วนไปเท่านั้น
สรุปเนื้อหา ในบทนี้ได้พูดถึงเกี่ยวกับอิเทอเรเตอร์ว่าเป็นสิ่งที่ใช้แทนออบเจ็กต์ชนิดกลุ่มและมีข้อดีกว่าตรงที่สร้างได้รวดเร็วและประหยัดพื้นที่
และยังได้แนะนำการสร้างเจเนอเรเตอร์ซึ่งเป็นอิเทอเรเตอร์อย่างง่าย
สำหรับในบทหน้าจะค่อยมาพูดถึงการสร้างอิเทอเรเตอร์ด้วยการสร้างคลาส ซึ่งเป็นวิธีที่ทั่วไปและมีขอบเขตการใช้งานกว้างขวางกว่า
อ้างอิง