Notes & software courses · Free to learn
Aph's Blog
On this page

เลือก for หรือ while ให้ถูก

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

หน้าก่อนสอนว่า for กับ while เขียนยังไง หน้านี้ตอบคำถามที่ยากกว่า คือจะรู้ได้ยังไงว่าโจทย์ตรงหน้าต้องใช้ตัวไหน

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

คำถามเดียวที่ตัดสินได้: ใครเป็นคนควบคุมการขยับ

ลืมคำอธิบายแบบ "for วนตามลำดับ while วนตามเงื่อนไข" ไปก่อน เพราะมันไม่ช่วยตัดสินใจอะไร ให้ถามแทนว่า ใครขยับตัวนับ

forwhile
ใครขยับตัวนับลำดับ (range หรือลิสต์) ขยับให้เราเราขยับเองด้วยมือ
ขยับได้แค่ไหนทีละหนึ่งก้าวตามลำดับที่กำหนดไว้ล่วงหน้า แทรกแซงไม่ได้อิสระ กระโดด ถอยหลัง หรือขยับหลายก้าวก็ได้
รู้จำนวนรอบก่อนเริ่มไหมรู้ (เท่ากับความยาวของลำดับ)ไม่รู้ก็ได้ ขึ้นกับว่าเงื่อนไขจะเท็จเมื่อไหร่
ต้องระวังอะไรแทบไม่มี ลืมขยับไม่ได้เพราะไม่ได้ขยับเองลืมขยับ = วนไม่จบตลอดกาล

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

พิสูจน์: แก้ตัวนับข้างใน for ไม่มีผลอะไรเลยpython
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 รอบ กระโดดข้ามได้ตามสั่ง")
Output
--- 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 เป็นตัวควบคุมจริง ๆ เพราะเงื่อนไขอ่านค่าจากมันตรง ๆ

เกณฑ์ตัดสินใน 1 บรรทัด

ถ้าการขยับไปข้างหน้าของโจทย์ ไม่ใช่ทีละหนึ่งเสมอ (ต้องกระโดด ถอย หรือขยับตามเงื่อนไขที่รู้ตอนรัน) ให้ใช้ while — นอกนั้นใช้ for เพราะปลอดภัยกว่าและอ่านง่ายกว่า

โจทย์เดียวกัน เขียนสองแบบ แล้วดูว่าอันไหนเหมาะ

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

python
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 งานนี้หนึ่งรอบของการทำงานคือ หนึ่งกลุ่ม ซึ่งยาวไม่เท่ากัน จึงต้องกระโดดข้ามทั้งกลุ่มในรอบเดียว

python
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

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

break, continue และ else ที่คนใช้ผิดกันเกือบทั้งหมด

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

คำสั่งใช้เมื่อประโยชน์ที่ได้
breakได้คำตอบแล้วไม่ต้องดูต่อ เช่นค้นหาแล้วเจอไม่เสียเวลาวนของที่เหลือ และสื่อชัดว่า "จบภารกิจ"
continueรอบนี้ไม่เข้าเกณฑ์ ข้ามไปเลยทำให้โค้ดตื้นลง ไม่ต้องเอาเนื้อหางานทั้งก้อนไปห่อใน if
else ของ loopอยากรู้ว่าวนจบแบบ ไม่เคย break เลยหรือเปล่าแทนตัวแปรธง (flag) เช่น found = False ได้ทั้งตัว

เรื่อง continue ให้ดูรูปร่างของโค้ดสองแบบนี้เทียบกัน สังเกตว่าแบบขวาเนื้องานหลักไม่ต้องย่อหน้าลึก อ่านง่ายกว่าเวลาเงื่อนไขคัดออกมีหลายข้อ

python
# แบบไม่ใช้ 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 จะถูกข้ามไปทั้งก้อน นี่คือเหตุผลทั้งหมดที่คำสั่งนี้มีอยู่

for-else กับสองเคสที่ต่างกันแค่มี break หรือไม่มีpython
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])
Output
nums = [4, 8, 16]  (ไม่มีเลขคี่)
  วนจบโดยไม่เคยเจอ -> ไม่มีเลขคี่เลย
nums = [4, 15, 16]  (มีเลขคี่)
  เจอเลขคี่: 15

เคสที่สองไม่พิมพ์บรรทัด "ไม่มีเลขคี่เลย" เพราะออกจากลูปด้วย break พูดให้จำง่ายคือ else ของ loop แปลว่า "วนจนหมดแล้วไม่เจอสิ่งที่หา" ซึ่งเป็นคู่กับ break เสมอ ถ้าในลูปไม่มี break อยู่เลย การเขียน else ก็ไม่มีความหมาย (มันจะทำงานทุกครั้ง เหมือนเขียนต่อท้ายลูปธรรมดา)

ถ้ายังไม่ชินกับ else ของ loop

ไม่ต้องรีบใช้ก็ได้ เพราะการเขียนเป็นฟังก์ชันแล้ว 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 ที่ต้องรู้ก่อนใช้: มันหยุดที่ลิสต์ที่สั้นกว่า โดยไม่แจ้งเตือนอะไรเลย

zip กินข้อมูลหายเงียบ ๆ เมื่อสองลิสต์ยาวไม่เท่ากันpython
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 ไม่มีคำเตือน")
Output
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) เมื่อสองลิสต์ควรยาวเท่ากัน