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



[python] ข้อควรระวังเมื่อมีการแก้ไขลิสต์ขณะใช้ for อาจทำให้เกิดการวนซ้ำไม่สิ้นสุดได้
เขียนเมื่อ 2019/01/12 15:30
แก้ไขล่าสุด 2024/02/22 10:50
ในการเขียนโปรแกรมที่มีการทำงานวนซ้ำเช่นใน for หรือ while สิ่งหนึ่งที่น่ากลัวที่อาจเกิดได้จากความรู้เท่าไม่ถึงการณ์ นั่นก็คือ "วังวนไม่สิ้นสุด" (infinite loop) นั่นเอง

ในบทความนี้จะมายกกรณีตัวอย่างหนึ่งที่อาจเกิดขึ้นได้เวลาใช้ for ใน python และวิธีป้องกันแก้ไข

ปกติแล้ว for จะถูกใช้เพื่อวนซ้ำเพื่อรับค่าตัวแปรแต่ละตัวที่อยู่ในลิสต์มาใช้ทีละตัว (รายละเอียดอ่านได้ใน ภาษาไพธอนเบื้องต้น บทที่ ๙)

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

หากได้ลองวิเคราะห์ปัญหาที่จะยกถึงต่อไปนี้เป็นอย่างดีแล้วก็จะเข้าใจหลักการทำงานของ for ได้ดีขึ้นและป้องกันการทำอะไรผิดพลาดได้

ลองยกตัวอย่างด้วยโค้ดสั้นๆง่ายๆนี้
lis = ['a','b','c']
for x in lis:
    lis += [x*2]
    # หรือไม่ก็ lis.append(x*2)

(ถ้าใครเผลอรันตามไปแล้วให้รีบกด ctrl+c เพื่อออก ไม่เช่นนั้นมันไม่มีวันหยุดเอง)

ในตัวอย่างนี้ลิสต์มีสมาชิกแค่ ๓ ตัว ถ้าคิดโดยทั่วไปแล้วน่าจะมีการวนแค่ ๓ รอบแล้วก็จบแค่นั้น

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

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

หลักการทำงานหากเขียนแทนด้วย while จะได้ใกล้เคียงกับแบบนี้ (ที่จริงไม่ได้เหมือนซะทีเดียว แค่เทียบให้เห็นภาพชัด)
i = 0
while(i<len(lis)):
    x = lis[i]
    lis += [x*2]
    i += 1

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

(เพียงแต่หาก for ใช้กับอิเทอเรเตอร์การทำงานจะต่างกันไปคนละแบบ รายละเอียดอ่านได้ใน ภาษาไพธอนเบื้องต้น บทที่ ๒๖)

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



วิธีแก้ไขเพื่อที่จะทำให้การเปลี่ยนแปลงภายในตัวลิสต์นั้นไม่ส่งผลอะไรต่อค่าที่วนใน for ก็คือ ให้ใส่ [:] ต่อท้ายลิสต์ แบบนี้
lis = ['a','b','c']
for x in lis[:]:
    lis += [x*2]
print(lis) # ได้ ['a', 'b', 'c', 'aa', 'bb', 'cc']

คราวนี้จะเห็นว่าเกิดการวนแค่ ๓ รอบตามที่ควรจะเป็น ไม่เกิดการวนแบบไม่สิ้นสุดแล้ว

การใส่ [] หลังลิสต์เป็นการระบุว่าเราจะเอาสมาชิกตัวไหนบ้างในลิสต์ไป และจะมีการสร้างลิสต์ใหม่ซึ่งบรรจุสมาชิกของลิสต์นั้นในตะแหน่งที่ระบุ

ปกติดัชนีจะใส่ในรูปของ [2:] หรือ [-2:] หรือ [2:-2] เพื่อระบุว่าจะเอาตัวแปรตั้งแต่ที่เท่าไหร่ถึงเท่าไหร่แบบนั้น

แต่การที่ใส่ดัชนีเป็น [:] หรือ [0:] นั้นจะมีความหมายว่าจะเราจะเอาทุกตัวในลิสต์

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

ยกตัวอย่างง่ายๆเพื่อเทียบความแตกต่างระหว่างใส่ [:] กับไม่ใส่
lisa = [1,2,3]
lisb = lisa
print(lisa==lisb) # ได้ True
print(lisa is lisb) # ได้ True
lisc = lisa[:]
print(lisa==lisc) # ได้ True
print(lisa is lisc) # ได้ False

lisb ไม่ได้ใส่ [:] จึงเป็นการนิยามว่า lisb ก็คือ lisa เลย ไม่ได้สร้างลิสต์ใหม่

แต่ lisc ใส่ [:] ดังนั้นจึงมีการสร้างลิสต์ใหม่ แม้จะมีค่าเท่ากับ lisa แต่ถ้าใช้ is ตรวจสอบดูจะได้ False ซึ่งบ่งบอกว่าเป็นออบเจ็กต์คนละตัว

ดังนั้น ถ้าเราใส่ [:] ลงไปหลังลิสต์ที่ใช้กับ for จะทำให้ต่อให้มีการเปลี่ยนแปลงอะไรกับลิสต์นั้นก็ไม่ทำให้ผลการวนใน for เปลี่ยนแปลงไปแล้ว

อย่างไรก็ตาม แม้จะเป็นการสร้างลิสต์ใหม่ก็ตาม แค่ค่าที่อยู่ข้างในยังเป็นตัวเดียวกัน ดังนั้น
print(lisa[0] is lisc[0]) # ได้ True


นอกจากนี้ ยังมีอีกวิธีนึงก็คือแบบนี้
lis = ['a','b','c']
for x in lis:
    lis = lis+[x*2]
print(lis) # ได้ ['a', 'b', 'c', 'aa', 'bb', 'cc']

เพราะว่าในกรณีนี้ ใน lis = lis+[x*2] นั้น lis ตัวซ้ายถือเป็นลิสต์คนละตัวกับ lis ตัวขวา จึงเป็นการสร้างลิสต์ใหม่ขึ้น ลิสต์ที่อยู่ในแปร lis หลังจากนั้นจึงเป็นตัวแปรใหม่ที่ไม่ได้เกี่ยวข้องกันกับ lis เดิม ลิสต์เดิมซึ่งใช้เป็นตัวป้อนเข้าของ for จึงไม่ได้เปลี่ยนแปลงไปแต่อย่างใด

lis = lis+[x*2] และ lis += [x*2] นั้นแม้ดูเผินๆแล้วจะคล้ายกัน แต่ผลที่เกิดขึ้นจริงๆต่างกันในรายละเอียด

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

แต่ถ้าใช้กับทูเพิลล่ะจะเป็นยังไง? ลองทดสอบดูได้
tup = ('a','b','c')
for x in tup:
    tup += (x*2,)
print(tup) # ได้ ('a', 'b', 'c', 'aa', 'bb', 'cc')

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

ดังนั้นแล้ว tup += (x*2,) จึงไม่ต่างจาก tup = tup+(x*2,) ยังไงก็เป็นการสร้างทูเพิลใหม่ขึ้นมา



เพื่อให้เห็นภาพชัด อีกตัวอย่างที่น่าแสดงให้เห็นก็คือ กรณีที่ไม่ใช่แค่เพิ่มสมาชิกต่อท้ายลิสต์ แต่เป็นการแทรกไว้ข้างหน้า แบบนี้ลำดับของสมาชิกในลิสต์จะมีการเปลี่ยนแปลง
lis = ['a','b','c']
for x in lis:
    lis.insert(0,x*2)
    print(lis)
    if(len(lis)>6): break # ต้องใส่ ไม่งั้นจะวนไม่สิ้นสุด

ได้
['aa', 'a', 'b', 'c']
['aa', 'aa', 'a', 'b', 'c']
['aa', 'aa', 'aa', 'a', 'b', 'c']
['aa', 'aa', 'aa', 'aa', 'a', 'b', 'c']

ตรงนี้แสดงให้เห็นว่าในการวนซ้ำทุกรอบ ตัวที่ถูกหยิบมาใส่ใน x คือ 'a' ตลอด เพราะเมื่อวนรอบแรก 'aa' ได้ถูกแทรกใส่ตำแหน่ง 0 แล้ว 'a' เดิมจึงเลื่อนไปอยู่ตำแหน่ง 1

พอรอบต่อมา for จะต้องไปหยิบตัวที่สอง (ตำแหน่ง 1) จึงหยิบ 'a' มาซ้ำอีกรอบ และเป็นแบบนี้ไปทุกรอบ ถ้าไม่สั่ง break ก่อน จะเกิดการหยิบ a ซ้ำไปเรื่อยๆแบบนี้ไม่มีที่สิ้นสุด

แต่ถ้าเกิดใส่ [:] ผลที่ได้ก็จะต่างกัน เพราะเป็นการสร้างลิสต์ใหม่
lis = ['a','b','c']
for x in lis[:]:
    lis.insert(0,x*2)
    print(lis)

ได้
['aa', 'a', 'b', 'c']
['bb', 'aa', 'a', 'b', 'c']
['cc', 'bb', 'aa', 'a', 'b', 'c']


แสดงว่าไม่ว่าลิสต์จะเปลี่ยนแปลงไปยังไงในการวนแต่ละครั้งก็ไปหยิบเอาสมาชิกจากลิสต์เดิมมาใช้

หรือแบบนี้ก็จะได้ผลเหมือนกัน
lis = ['a','b','c']
for x in lis:
    lis = [x*2]+lis
    print(lis)
เพราะการเขียนแบบนี้ก็เป็นการสร้างลิสต์ขึ้นมาใหม่ในทุกรอบที่มีการวนซ้ำ



อ้างอิง
https://qiita.com/Feyris_nyan/items/aa85772aa2ba8e07c87e


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

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

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

หมวดหมู่

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

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

สารบัญ

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

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

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



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

  ค้นหาบทความ

  บทความแนะนำ

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

ไทย

日本語

中文