φυβλαςのβλογ
บล็อกของ 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)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

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

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月

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

ไทย

日本語

中文