On this page
เลือก for หรือ while ให้ถูก
หน้าก่อนสอนว่า for กับ while เขียนยังไง หน้านี้ตอบคำถามที่ยากกว่า คือจะรู้ได้ยังไงว่าโจทย์ตรงหน้าต้องใช้ตัวไหน
คนที่เพิ่งเรียน loop มักติดปัญหาเดียวกันหมด: รู้ว่า for เขียนยังไง รู้ว่า while เขียนยังไง แต่พอเจอโจทย์จริงแล้วเลือกไม่ถูกว่าควรใช้ตัวไหน สุดท้ายก็ใช้ for ทุกอย่างเพราะคุ้นกว่า แล้วไปเจอทางตันทีหลังโดยไม่รู้ว่าทางตันนั้นเกิดจากการเลือกเครื่องมือผิดตั้งแต่แรก หน้านี้จะให้เกณฑ์ตัดสินที่ใช้ได้จริงกับทุกโจทย์
คำถามเดียวที่ตัดสินได้: ใครเป็นคนควบคุมการขยับ
ลืมคำอธิบายแบบ "for วนตามลำดับ while วนตามเงื่อนไข" ไปก่อน เพราะมันไม่ช่วยตัดสินใจอะไร ให้ถามแทนว่า ใครขยับตัวนับ
| for | while | |
|---|---|---|
| ใครขยับตัวนับ | ลำดับ (range หรือลิสต์) ขยับให้เรา | เราขยับเองด้วยมือ |
| ขยับได้แค่ไหน | ทีละหนึ่งก้าวตามลำดับที่กำหนดไว้ล่วงหน้า แทรกแซงไม่ได้ | อิสระ กระโดด ถอยหลัง หรือขยับหลายก้าวก็ได้ |
| รู้จำนวนรอบก่อนเริ่มไหม | รู้ (เท่ากับความยาวของลำดับ) | ไม่รู้ก็ได้ ขึ้นกับว่าเงื่อนไขจะเท็จเมื่อไหร่ |
| ต้องระวังอะไร | แทบไม่มี ลืมขยับไม่ได้เพราะไม่ได้ขยับเอง | ลืมขยับ = วนไม่จบตลอดกาล |
ข้อที่สำคัญที่สุดคือแถวที่สอง และมันไม่ใช่เรื่องสไตล์การเขียน แต่เป็นข้อจำกัดจริงของภาษา หลายคนคิดว่าถ้าอยากให้ for กระโดด ก็แก้ค่าตัวนับข้างในลูปสิ — ลองรันดูว่าเกิดอะไรขึ้น
nums = [10, 20, 30, 40, 50]
print("--- for: พยายามแก้ i ข้างในลูป")
for i in range(len(nums)):
print(" รอบนี้ i =", i)
i += 2 # พยายามกระโดด -> ไม่มีผลอะไรเลย
print(" สรุป: วนครบ 5 รอบเหมือนเดิม บรรทัด i += 2 เหมือนไม่มีอยู่")
print("--- while: เราขยับเอง")
i = 0
while i < len(nums):
print(" รอบนี้ i =", i)
i += 2 # กระโดดได้จริง
print(" สรุป: วน 3 รอบ กระโดดข้ามได้ตามสั่ง")--- for: พยายามแก้ i ข้างในลูป
รอบนี้ i = 0
รอบนี้ i = 1
รอบนี้ i = 2
รอบนี้ i = 3
รอบนี้ i = 4
สรุป: วนครบ 5 รอบเหมือนเดิม บรรทัด i += 2 เหมือนไม่มีอยู่
--- while: เราขยับเอง
รอบนี้ i = 0
รอบนี้ i = 2
รอบนี้ i = 4
สรุป: วน 3 รอบ กระโดดข้ามได้ตามสั่งเหตุผลคือ for หยิบค่าถัดไปจากลำดับมาใส่ตัวแปร i ใหม่ทุกรอบ ค่าที่เราเขียนทับลงไปเองจึงถูกโยนทิ้งทันทีเมื่อขึ้นรอบใหม่ พูดอีกแบบคือ i ใน for เป็นแค่ ที่พักค่า ไม่ใช่ตัวควบคุมการวน ส่วน i ใน while เป็นตัวควบคุมจริง ๆ เพราะเงื่อนไขอ่านค่าจากมันตรง ๆ
ถ้าการขยับไปข้างหน้าของโจทย์ ไม่ใช่ทีละหนึ่งเสมอ (ต้องกระโดด ถอย หรือขยับตามเงื่อนไขที่รู้ตอนรัน) ให้ใช้ while — นอกนั้นใช้ for เพราะปลอดภัยกว่าและอ่านง่ายกว่า
โจทย์เดียวกัน เขียนสองแบบ แล้วดูว่าอันไหนเหมาะ
งานแรก: บวกเลขทุกตัวในลิสต์ จำนวนรอบเท่ากับความยาวลิสต์เป๊ะ ๆ ไม่มีเงื่อนไขให้กระโดด
nums = [4, 8, 15]
# แบบ for — เหมาะ
total = 0
for n in nums:
total += n
# แบบ while — ทำได้ แต่ต้องดูแลตัวนับเองโดยไม่ได้ประโยชน์อะไรเพิ่ม
total = 0
i = 0
while i < len(nums):
total += nums[i]
i += 1 # ถ้าลืมบรรทัดนี้ โปรแกรมค้างทันทีงานที่สอง: นับกลุ่มตัวอักษรซ้ำที่ติดกัน เช่น aabbbcc ต้องได้ a2 b3 c2 งานนี้หนึ่งรอบของการทำงานคือ หนึ่งกลุ่ม ซึ่งยาวไม่เท่ากัน จึงต้องกระโดดข้ามทั้งกลุ่มในรอบเดียว
s = "aabbbcc"
# แบบ while — เหมาะ เพราะกระโดดข้ามทั้งกลุ่มได้
i = 0
while i < len(s):
ch = s[i]
count = 0
while i < len(s) and s[i] == ch:
count += 1
i += 1 # กระโดดทีละกลุ่ม ไม่ใช่ทีละตัว
print(ch, count)
# แบบ for — ทำได้ แต่ต้องเพิ่มตัวแปรจำสถานะ แล้วโค้ดจดกลุ่มจะซ้ำสองที่
# (ที่หนึ่งในลูป อีกที่หลังลูปจบ เพราะกลุ่มสุดท้ายไม่มีตัวที่ต่างมาปิดท้าย)
# -> ลืมชุดหลังลูปคือบั๊กที่พบบ่อยที่สุดของโจทย์แนวนี้ถ้าเขียน for แล้วพบว่าต้องเพิ่มตัวแปรจำสถานะหลายตัว หรือต้องก๊อปโค้ดชุดเดิมไปวางซ้ำหลังลูปอีกที นั่นคือสัญญาณว่าโจทย์นี้ควรใช้ while ตั้งแต่แรก โค้ดจะสั้นลงและบั๊กหายไปเอง
break, continue และ else ที่คนใช้ผิดกันเกือบทั้งหมด
สามคำนี้มักถูกสอนแค่ว่าทำอะไร แต่ไม่ได้บอกว่าควรหยิบมาใช้เมื่อไหร่ ซึ่งเป็นส่วนที่ทำให้โค้ดอ่านง่ายขึ้นหรือแย่ลงจริง ๆ
| คำสั่ง | ใช้เมื่อ | ประโยชน์ที่ได้ |
|---|---|---|
| break | ได้คำตอบแล้วไม่ต้องดูต่อ เช่นค้นหาแล้วเจอ | ไม่เสียเวลาวนของที่เหลือ และสื่อชัดว่า "จบภารกิจ" |
| continue | รอบนี้ไม่เข้าเกณฑ์ ข้ามไปเลย | ทำให้โค้ดตื้นลง ไม่ต้องเอาเนื้อหางานทั้งก้อนไปห่อใน if |
| else ของ loop | อยากรู้ว่าวนจบแบบ ไม่เคย break เลยหรือเปล่า | แทนตัวแปรธง (flag) เช่น found = False ได้ทั้งตัว |
เรื่อง continue ให้ดูรูปร่างของโค้ดสองแบบนี้เทียบกัน สังเกตว่าแบบขวาเนื้องานหลักไม่ต้องย่อหน้าลึก อ่านง่ายกว่าเวลาเงื่อนไขคัดออกมีหลายข้อ
# แบบไม่ใช้ continue — เนื้องานถูกห่อไว้ในสุด ยิ่งมีเงื่อนไขคัดออกมาก ยิ่งลึก
for user in users:
if user.active:
if user.age >= 18:
send_email(user)
# แบบใช้ continue เป็นตัวคัดออกด้านหน้า (guard) — เนื้องานอยู่ระดับตื้นสุด
for user in users:
if not user.active:
continue
if user.age < 18:
continue
send_email(user)else ของ loop ทำงานอย่างไรจริง ๆ
จุดที่คนเข้าใจผิดกันมากที่สุด: else ของ loop ไม่ใช่ "ทำเมื่อ loop จบ" เฉย ๆ เพราะถ้าเป็นแบบนั้นก็เขียนต่อท้ายลูปธรรมดาก็ได้ ไม่ต้องมี else ให้เปลืองคำสั่ง ความหมายจริงคือ ทำเมื่อวนจบโดยไม่เคยเจอ break — ถ้าออกจากลูปด้วย break บล็อก else จะถูกข้ามไปทั้งก้อน นี่คือเหตุผลทั้งหมดที่คำสั่งนี้มีอยู่
def find_odd(nums):
for n in nums:
if n % 2 == 1:
print(" เจอเลขคี่:", n)
break
else:
print(" วนจบโดยไม่เคยเจอ -> ไม่มีเลขคี่เลย")
print("nums = [4, 8, 16] (ไม่มีเลขคี่)")
find_odd([4, 8, 16])
print("nums = [4, 15, 16] (มีเลขคี่)")
find_odd([4, 15, 16])nums = [4, 8, 16] (ไม่มีเลขคี่)
วนจบโดยไม่เคยเจอ -> ไม่มีเลขคี่เลย
nums = [4, 15, 16] (มีเลขคี่)
เจอเลขคี่: 15เคสที่สองไม่พิมพ์บรรทัด "ไม่มีเลขคี่เลย" เพราะออกจากลูปด้วย break พูดให้จำง่ายคือ else ของ loop แปลว่า "วนจนหมดแล้วไม่เจอสิ่งที่หา" ซึ่งเป็นคู่กับ break เสมอ ถ้าในลูปไม่มี break อยู่เลย การเขียน else ก็ไม่มีความหมาย (มันจะทำงานทุกครั้ง เหมือนเขียนต่อท้ายลูปธรรมดา)
ไม่ต้องรีบใช้ก็ได้ เพราะการเขียนเป็นฟังก์ชันแล้ว return ทันทีที่เจอ ให้ผลเหมือนกันและคนอ่านเข้าใจง่ายกว่าในโค้ดจริง แต่ต้องรู้ว่ามันหมายถึงอะไร เพราะจะเจอในโค้ดคนอื่น และถ้าเข้าใจผิดว่า "มันรันทุกครั้งที่ loop จบ" จะอ่านโค้ดนั้นผิดทั้งก้อน
เดินบนข้อมูล: มี 4 ท่า เลือกให้ถูกท่า
พอตัดสินใจได้ว่าใช้ for แล้ว ยังต้องเลือกอีกชั้นว่าจะเดินแบบไหน ท่าที่ผิดไม่ได้ทำให้โค้ดพัง แต่ทำให้โค้ดยาวและพลาดง่ายขึ้นโดยไม่จำเป็น
| ท่า | ใช้เมื่อ | หมายเหตุ |
|---|---|---|
| for x in a | ต้องการแค่ค่า ไม่สนตำแหน่ง | ท่ามาตรฐาน ใช้บ่อยสุด เลือกอันนี้ก่อนเสมอถ้าทำได้ |
| for i, x in enumerate(a) | ต้องการทั้งตำแหน่งและค่า | ดีกว่า for i in range(len(a)) แล้วไปหยิบ a[i] เอง เพราะสั้นกว่าและพลาดยากกว่า |
| for i in range(len(a)) | ต้องใช้ index ไปทำอย่างอื่น เช่นเทียบกับ a[i-1] หรือเขียนค่าลง a[i] | เหลือไว้ใช้เฉพาะกรณีนี้ ไม่ใช่ท่าเริ่มต้น |
| for x, y in zip(a, b) | มีข้อมูลสองชุดที่คู่กันตำแหน่งต่อตำแหน่ง | ระวังกับดักด้านล่าง |
กับดักของ zip ที่ต้องรู้ก่อนใช้: มันหยุดที่ลิสต์ที่สั้นกว่า โดยไม่แจ้งเตือนอะไรเลย
names = ["Aph", "Bee", "Cha"]
scores = [80, 95] # สั้นกว่า names หนึ่งช่อง
for name, score in zip(names, scores):
print(name, score)
print("ได้", len(list(zip(names, scores))), "คู่ จากชื่อทั้งหมด", len(names), "คน")
print("-> Cha หายไปเงียบ ๆ ไม่มี error ไม่มีคำเตือน")Aph 80
Bee 95
ได้ 2 คู่ จากชื่อทั้งหมด 3 คน
-> Cha หายไปเงียบ ๆ ไม่มี error ไม่มีคำเตือนพฤติกรรมนี้ไม่ใช่บั๊กของ Python และบางครั้งเราก็ต้องการแบบนี้จริง ๆ แต่ถ้าเราคิดว่าสองลิสต์ยาวเท่ากันแน่ ๆ แล้วมันไม่เท่า ข้อมูลจะหายไปโดยที่โปรแกรมยังทำงานปกติ ซึ่งหาเจอยากมาก ทางที่ปลอดภัยคือเช็ค len(a) == len(b) ก่อน zip เมื่อความยาวควรเท่ากันตามตรรกะของงาน
สรุปหัวข้อนี้
- คำถามตัดสิน for หรือ while คือ "การขยับไปข้างหน้าเป็นทีละหนึ่งเสมอหรือไม่" ไม่ใช่ "รู้จำนวนรอบไหม" อย่างเดียว
- แก้ค่าตัวนับข้างใน for ไม่มีผล เพราะมันถูกเขียนทับด้วยค่าถัดไปของลำดับทุกรอบ ถ้าต้องกระโดดต้องใช้ while
- ต้องเพิ่มตัวแปรจำสถานะหลายตัว หรือต้องก๊อปโค้ดไปวางซ้ำหลังลูป = สัญญาณว่าเลือก for ผิด
- break ใช้เมื่อได้คำตอบแล้ว, continue ใช้คัดออกด้านหน้าเพื่อให้โค้ดตื้นลง
- else ของ loop แปลว่า "วนจบโดยไม่เคย break" ไม่ใช่ "ทำเมื่อ loop จบ" — ถ้าในลูปไม่มี break การเขียน else ก็ไร้ความหมาย
- เดินบนข้อมูลให้เริ่มจาก for x in a ก่อน แล้วขยับไป enumerate หรือ range(len(a)) เฉพาะเมื่อจำเป็น
- zip หยุดที่ลิสต์สั้นกว่าแบบเงียบ ๆ — เช็คความยาวก่อนถ้ามันควรเท่ากัน
1) เขียนโค้ดพิมพ์เลขคี่จาก 1 ถึง 99 สองแบบ ทั้ง for และ while แล้วตอบว่าแบบไหนเหมาะกว่าเพราะอะไร 2) รับตัวเลขเรื่อย ๆ จนผู้ใช้พิมพ์ 0 — ข้อนี้ใช้ for ได้ไหม ทำไม 3) เขียนฟังก์ชันหาว่าคำหนึ่งมีสระซ้ำติดกันไหม โดยต้องกระโดดข้ามทั้งกลุ่มที่ซ้ำ 4) เขียนค้นหาชื่อในลิสต์ด้วย for-else แล้วเขียนอีกรอบด้วยฟังก์ชัน + return เทียบกันว่าอันไหนอ่านง่ายกว่า 5) แก้โค้ดนี้ให้ปลอดภัย: for a, b in zip(list1, list2) เมื่อสองลิสต์ควรยาวเท่ากัน