φυβλαςのβλογ
phyblasのブログ



ภาษา python เบื้องต้น บทที่ ๒๖: อิเทอเรเตอร์และเจเนอเรเตอร์
เขียนเมื่อ 2016/04/26 22:57
แก้ไขล่าสุด 2024/02/22 11:07
 

ในบทนี้จะพูดถึงเรื่องของอิเทอเรเตอร์ (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 แบบนี้เจเนอเรเตอร์จะประหยัดเวลากว่ามากเพราะเสียเวลาสร้างข้อมูลแค่เท่ากับ จำนวนครั้งที่วนไปเท่านั้น



สรุปเนื้อหา

ในบทนี้ได้พูดถึงเกี่ยวกับอิเทอเรเตอร์ว่าเป็นสิ่งที่ใช้แทนออบเจ็กต์ชนิดกลุ่มและมีข้อดีกว่าตรงที่สร้างได้รวดเร็วและประหยัดพื้นที่
และยังได้แนะนำการสร้างเจเนอเรเตอร์ซึ่งเป็นอิเทอเรเตอร์อย่างง่าย

สำหรับในบทหน้าจะค่อยมาพูดถึงการสร้างอิเทอเรเตอร์ด้วยการสร้างคลาส ซึ่งเป็นวิธีที่ทั่วไปและมีขอบเขตการใช้งานกว้างขวางกว่า



อ้างอิง




-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

目次

日本による名言集
モジュール
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
機械学習
-- ニューラル
     ネットワーク
javascript
モンゴル語
言語学
maya
確率論
日本での日記
中国での日記
-- 北京での日記
-- 香港での日記
-- 澳門での日記
台灣での日記
北欧での日記
他の国での日記
qiita
その他の記事

記事の類別



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  記事を検索

  おすすめの記事

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文