φυβλαςのβλογ
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
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



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

  查看日志

  推荐日志

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