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



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

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

แต่เจเนอเรเตอร์นั้นเป็นเพียงอิเทอเรเตอร์ชนิดหนึ่งที่มีรูปแบบการสร้างที่ค่อนข้างง่าย

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



การสร้างคลาสของอิเทอเรเตอร์

การสร้างคลาสของอิเทอเรเตอร์ก็ทำได้เช่นเดียวกับคลาสของออบเจ็กต์ทั่วไป แต่ว่าจำเป็นจะต้องมีเมธอดที่สำคัญ ๒ ตัวรวมอยู่ด้วย นั่นคือ __iter__ และ __next__

ขอเริ่มจากการยกตัวอย่างการสร้างคลาสอิเทอเรเตอร์อย่าง ง่ายๆสั้นๆที่สุดก่อนที่จะเริ่มอธิบายว่า __iter__ กับ __next__ มีหน้าที่ไว้ทำอะไร
class Iternap:
    def __init__(self):
        self.n = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.n += 1        
        return self.n

คลาส Iternap ในตัวอย่างนี้คือคลาสของอิเทอเรเตอร์ที่ใช้นับจำนวนตัวเลข โดยจะคืนค่าตัวเลข 1, 2, 3, ... ไปเรื่อยๆ มีลักษณะการทำงานเหมือนกับเจเนอเรเตอร์จากฟังก์ชัน gennap ซึ่งเขียนถึงไปในบทที่แล้ว (ให้ย้อนกลับไปดูแล้วเทียบกัน ไม่ขอยกมาเขียนซ้ำ)

ลองเริ่มนำมาใช้งานดูเลย
nap = Iternap()
print(next(nap)) # ได้ 1
print(next(nap)) # ได้ 2
print(next(nap)) # ได้ 3

จะเห็นว่าเมื่อใช้ฟังก์ชัน next จะมีการคืนค่าตัวเลขที่เพิ่มขึ้นตามลำดับ นี่เป็นอิเทอเรเตอร์ที่ให้ผลเช่นเดียวกับเจเนอเรเตอร์

คราวนี้มาลองดูโค้ดที่ใช้ในการสร้างคลาส Iternap จะเห็นว่ามีการนิยามเมธอดขึ้นมา ๓ อัน คือ __init__, __iter__ และ __next__

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

ส่วนเมธอด __next__ คือเมธอดที่จะทำงานเมื่อมีการใช้ฟังก์ชัน next นั่นเอง ซึ่งก็คล้ายกับเมธอดทั้งหลายที่ได้พูดถึงไปในบทที่ ๒๔ เช่นเมธอด __len__, __str__ และ __bool__ ทำงานเมื่อใช้ฟังก์ชัน len, str และ bool ตามลำดับ

ลองพิมพ์
print(nap.__next__()) # ได้ 4
print(nap.__next__()) # ได้ 5

จะเห็นว่า nap.__next__() ทำงานเหมือนกับ next(nap) นั่นคือเพิ่มค่าแอตทริบิวต์ n ทีละ 1 แล้วคืนค่ากลับมา

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

ขอเน้นว่าคำสั่งสำหรับส่งค่าคืนกลับ ที่ใช้ในเมธอด __next__ คือ return เหมือนฟังก์ชันทั่วไป ไม่ใช่ yield เหมือนอย่างที่ใช้ในฟังก์ชันสร้างเจเนอเรเตอร์ คำสั่ง yield จะใช้กับเจเนอเรเตอร์เท่านั้น

***ในไพธอน 2 เมธอด __next__ นี้ชื่อ next เฉยๆ ไม่มีขีดล่างสองตัวขนาบ
>>> รายละเอียด



เมธอด __iter__

ส่วนเมธอด __iter__ นั้นก็ทำนองเดียวกันกับ __next__ มันคือเมธอดที่จะถูกเรียกใช้โดยฟังก์ชันชื่อ iter

ฟังก์ชัน iter นี้ไม่ได้กล่าวถึงตั้งแต่บทที่แล้ว ความจริงแล้วเมธอด __iter__ นี้อาจไม่จำเป็นต้องมีก็ได้หากเราจะใช้แต่ฟังก์ชัน next เพื่อเดินหน้าอิเทอเรเตอร์ต่อไปเรื่อยๆ

แต่ __iter__ จะจำเป็นในกรณีที่ต้องการใช้ใน for หรือฟังก์ชันจำพวก map และ filter

เวลาที่ใช้คำสั่ง for เมธอด __iter__ จะถูกเรียกใช้โดยอัตโนมัติเพื่อเป็นการยืนยันว่าออบเจ็กต์นี้เป็นอิเทอเรเตอร์

หากไม่มีการนิยามเมธอด __iter__ เวลาใช้ for จะเกิดข้อผิดพลาดขึ้นทันที
class Itermaidai:
    def __init__(self):
        self.n = 0
    def __next__(self):
        self.n += 1
        return self.n

for it in Itermaidai():
    print(it)

ได้
TypeError: 'Itermaidai' object is not iterable

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

หลังจากที่เรียก __iter__ แล้ว ต่อมาก็จะมีการเรียก __next__ แล้วก็เริ่มการทำงานวนซ้ำใน for แล้วก็เรียก __next__ ไปเรื่อยๆทุกครั้ง

ลองเอา Iternap ที่นิยามตอนแรกสุดนี้มาใช้กับ for ดู
for it in Iternap():
    if(it>10): break
    print(it,end='>')

ได้
1>2>3>4>5>6>7>8>9>10>

ซึ่งจะมีค่าเทียบเท่ากับการเขียนแบบนี้
it = Iternap()
it.__iter__()
i = 1
while(i<=10):
    print(it.__next__(),end='>')
    i += 1

หรือหากใช้ฟังก์ชัน iter กับ next แทนเมธอด __iter__ และ __next__ ก็จะเป็นแบบนี้
it = Iternap()
iter(it)
i = 1
while(i<=10):
    print(next(it),end='>')
    i += 1

จะเห็นว่าที่จริง it.__iter__() หรือ iter(it) นั้นไม่ได้จำเป็นต้องใส่ เพราะไม่ได้ทำหน้าที่อะไร ที่ใส่ไว้ในที่นี้ก็เพื่อให้เทียบเท่ากับการใช้ for เท่านั้น

อนึ่ง จะเห็นว่าในการใช้ for จะต้องใส่ break ไว้เพราะอิเทอเรเตอร์ที่นิยามขึ้นในครั้งนี้ไม่ได้กำหนดจุดจบเอาไว้



การกำหนดจุดสิ้นสุดของอิเทอเรเตอร์

ในกรณีของเจเนอเรเตอร์การสิ้นสุดจะเกิดขึ้นเมื่อฟังก์ชันอ่านโค้ดจนถึงสุดโดยที่ไม่เจอ yield

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

ตัวอย่าง ลองสร้างคลาสของอิเทอเรเตอร์สำหรับนับถอยหลัง
class Iternapthoilang:
    def __init__(self,n):
        self.n = n+1 # ตั้งค่าจำนวนเริ่มต้นตามจำนวนที่ป้อนเข้าไปในพารามิเตอร์
    def __iter__(self):
        return self
    def __next__(self):
        self.n -= 1 # ลดลงทีละ 1 ทุกรอบ
        if(self.n==0):
            raise StopIteration # หากเหลือ 0 ให้หยุด
        return self.n

for it in Iternapthoilang(10):
    print(it,end='>') 

จะเห็นว่าตัวอย่างนี้ในเมธอด __next__ มีการตั้งให้ถ้า n เหลือ 0 จะทำคำสั่ง raise เพื่อส่งข้อผิดพลาดชนิด StopIteration ออกไปเพื่อเป็นสัญญาณให้สิ้นสุดการวนซ้ำ

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

พออิเทอเรเตอร์มีจุดสิ้นสุดแล้วแบบนี้ก็สามารถนำมาใช้กับ for ได้ง่ายโดยไม่ต้องใส่ break รวมถึงสามารถใช้กับฟังก์ชันอย่าง map หรือ filter ได้



ตอนนี้เราสามารถอิเทอเรเตอร์สำหรับสร้างลำดับฟีโบนัชชีเช่นเดียวกับที่ใช้เจเนอเรเตอร์ในบทที่แล้วสร้างขึ้นได้
class Iterfib:
    def __init__(self,n):
       self.n = n
       self.a = 1
       self.b = 1
       self.i = 1
    def __iter__(self):
       return self
    def __next__(self):
        if(self.i<=self.n):
            x = self.a
            self.a, self.b = self.b, self.a+self.b
            self.i += 1
            return x
        else:
            raise StopIteration

for it in Iterfib(7):
    print(it,end='>>')

ได้
1>>1>>2>>3>>5>>8>>13>> 



การทำให้อิเทอเรเตอร์เริ่มใหม่ทุกครั้งที่เริ่มวนซ้ำ

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

อิเทอเรเตอร์คลาส Iterfib ที่สร้างขึ้นมาในตัวอย่างที่แล้วเองก็มีคุณสมบัติแบบเดียวกัน

ลอง
ite = Iterfib(7)
for it in ite:
    print(it,end='~~')
for it in ite:
    print(it,end='^^') 

จะได้
1~~1~~2~~3~~5~~8~~13~~

นั่นคือจะเห็นว่า for ตัวหลังไม่ทำงาน เพราะออบเจ็กต์อิเทอเรเตอร์ ite นั้นได้ถูกใช้งานหมดตั้งแต่ for ตัวแรกแล้ว

แต่เราสามารถเขียนให้อิเทอเรเตอร์ทำงานใหม่ทุกครั้งที่เริ่ม for ได้ โดยการแก้โครงสร้างคลาสเล็กน้อยดังนี้
class Iterfib:
    def __init__(self,n):
       self.n = n
    def __iter__(self):
       self.a = 1 # กำหนดค่าเริ่มต้น
       self.b = 1
       self.i = 1
       return self
    def __next__(self):
        if(self.i<=self.n):
            x = self.a
            self.a, self.b = self.b, self.a+self.b
            self.i += 1
            return x
        else:
            raise StopIteration

ite = Iterfib(7)
for it in ite:
    print(it,end='~~')
for it in ite:
    print(it,end='^^') 

ได้
1~~1~~2~~3~~5~~8~~13~~1^^1^^2^^3^^5^^8^^13^^

ข้อแตกต่างจาก Iterfib ตัวเดิมก็คือจะเห็นว่าส่วนที่กำหนดค่าแอตทริบิวต์ a, b และ i ตอนเริ่มต้นนั้นย้ายจากในเมธอด __init__ มาอยู่ในเมธอด __iter__ แทน

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

ในที่นี้จึงใส่คำสั่งให้แอตทริบิวต์ a, b และ i กลับมายังจุดเริ่มต้น ทำให้ทุกครั้งที่เริ่มวนซ้ำด้วย for อิเทอเรเตอร์จะถูกเริ่มนับใหม่ ทำให้สามารถใช้งานกี่ครั้งก็ได้ เช่นเดียวกับการใช้ลิสต์วนซ้ำใน for



เมธอด __reversed__ และ __len__

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

แต่สำหรับอิเทอเรเตอร์นั้นโดยปกติจะไม่สามารถใช้ฟังก์ชัน reversed ได้ อย่างไรก็ตามก็สามารถทำให้ใช้งานได้โดยใส่เมธอด __reversed__ ลงไปตอนสร้าง

นอกจากนี้ก็ยังมีฟังก์ชัน len ซึ่งสามารถใช้วัดความยาวได้แค่ลิสต์ แต่หากจะให้อิเทอเรเตอร์ที่สร้างมาใช้ได้ก็ต้องใส่เมธอด __len__ ให้

ตัวอย่าง การสร้างอิเทอเรเตอร์สำหรับสร้างตัวเลขไล่ตั้งแต่อาร์กิวเมนต์ตัวแรกถึงตัวหลัง โดยใส่เมธอด __len__ และ __reversed__ ลงไปเพื่อให้สามารถวัดความยาวและไล่ถอยหลังได้
class Iternaplai:
    def __init__(self,a=0,b=100):
        self.a = a
        self.b = b
    def __iter__(self):
        self.i = self.a-1.
        return self
    def __next__(self):
        self.i += 1
        if(self.i>self.b):
            raise StopIteration
        return self.i
    def __len__(self):
        return len([x for x in self])
    def __reversed__(self):
        return reversed([x for x in self])

print(len(Iternaplai(11,19))) # ได้ 9
print(list(reversed(Iternaplai(11,19)))) # ได้ [19.0, 18.0, 17.0, 16.0, 15.0, 14.0, 13.0, 12.0, 11.0] 



การใช้ฟังก์ชัน iter เพื่อสร้างอิเทอเรเตอร์จากออบเจ็กต์ชนิดกลุ่ม

ฟังก์ชัน iter นั้นนอกจากจะใช้กับออบเจ็กต์ชนิดกลุ่มได้ ซึ่งผลที่ได้ก็คือจะคืนตัวออบเจ็กต์นั้นกลับมาในรูปของอิเทอเรเตอร์

ลองใช้กับลิสต์ดูจะได้ข้อมูลที่มีคลาสเป็น list_iterator
list_it = iter([1,2,3]) 
print(type(list_it)) # ได้ <class 'list_iterator'>
print(list_it) # ได้  

หรือจะเขียน list_it = [1,2,3].__iter__() ก็ได้ เพราะฟังก์ชัน iter คือการเรียกใช้เมธอด __iter__ และเมธอดนี้มีอยู่ในออบเจ็กต์ชนิดกลุ่มอยู่แล้ว

อิเทอเรเตอร์ที่ได้มานี้จะเป็นตัววนซ้ำเพื่อส่งค่าคืนกลับมาเป็นสมาชิกในนั้นทีละตัว

การใช้งานของมันก็คือใช้กับ for เช่นเดียวกับตัวลิสต์ที่เป็นต้นแบบ และผลที่ได้ก็เหมือนกัน
for i in list_it:
    print(i, end='-')

ได้
1-2-3-

เพียงแต่ว่าหากจะแปลงเป็นอิเทอเรเตอร์เพื่อจะมาใช้ใน for ก็อาจไม่ค่อยมีประโยชน์นัก เพราะตัวลิสต์เองสามารถใช้ใน for ได้โดยตรงอยู่แล้ว

ดังนั้น for i in iter([1,2,3]): กับ for i in [1,2,3]: มีค่าเท่ากัน

ลองใช้กับทูเพิล, เซ็ต, ดิกชันนารี, สายอักขระ และเรนจ์ ผลที่ได้ก็จะเป็นในทำนองเดียวกัน
tuple_it = iter((4,5,6))
print(tuple_it) # ได้ 
set_it = iter({7,8,9})
print(set_it) # ได้ 
dict_it = iter({'ก':1,'ข':2,'ค':3})
print(dict_it) # ได้ 
str_it = iter('กขค')
print(str_it) # ได้ 
range_it = iter(range(9))
print(range_it) # ได้  

ในทางกลับกันอิเทอเรเตอร์เหล่านี้ก็สามารถแปลงกลับเป็นข้อมูลชนิดกลุ่มได้ แต่จะใช้ได้แค่ครั้งเดียว
print(set(tuple_it)) # ได้ {4, 5, 6}
print(tuple(range_it)) # ได้ (0, 1, 2, 3, 4, 5, 6, 7, 8)
print(list(range_it)) # ได้ [] เพราะถูกใช้ได้ครั้งเดียว



อ้างอิง




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

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

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

หมวดหมู่

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

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

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

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

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



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

  ค้นหาบทความ

  บทความแนะนำ

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

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

2024年

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

2023年

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

2022年

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

2021年

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

2020年

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

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

ไทย

日本語

中文