ในการเขียนโปรแกรมที่มีการทำงานวนซ้ำเช่นใน 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