φυβλαςのβλογ
บล็อกของ phyblas



ภาษา python เบื้องต้น บทที่ ๒๖: อิเทอเรเตอร์และเจเนอเรเตอร์
เขียนเมื่อ 2016/04/26 22:57
ในบทนี้จะพูดถึงเรื่องของอิเทอเรเตอร์ (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

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

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
python
-- numpy
-- matplotlib

-- pandas
-- pytorch
maya
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
เรียนภาษาจีน
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



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

  ค้นหาบทความ

  บทความแนะนำ

หลักการเขียนทับศัพท์ภาษาจีนกวางตุ้ง
การใช้ unix shell เบื้องต้น ใน linux และ mac
หลักการเขียนทับศัพท์ภาษาจีนกลาง
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
บันทึกการเที่ยวสวีเดน 1-12 พ.ค. 2014
แนะนำองค์การวิจัยและพัฒนาการสำรวจอวกาศญี่ปุ่น (JAXA)
เล่าประสบการณ์ค่ายอบรมวิชาการทางดาราศาสตร์โดยโซวเคนได 10 - 16 พ.ย. 2013
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
บันทึกการเที่ยวญี่ปุ่นครั้งแรกในชีวิต - ทุกอย่างเริ่มต้นที่สนามบินนานาชาติคันไซ
หลักการเขียนคำทับศัพท์ภาษาญี่ปุ่น
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ
ทำไมถึงอยากมาเรียนต่อนอก
เหตุผลอะไรที่ต้องใช้ภาษาวิบัติ?

บทความแต่ละเดือน

2019年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2018年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2017年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2016年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2015年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文