User Tools

Site Tools


python:debugging

A. การดีบัก

เวลาที่เราดีบัก เราควรจะแยกชนิดของข้อผิดพลาดให้ออก เพื่อจะได้หาสาเหตุมันได้เร็วขึ้น:

  • ข้อผิดพลาดเชิงวากยสัมพันธ์ (syntax errors) จะเห็นชัด จากที่ไพธอนอินเตอร์พรีเตอร์แจ้งข้อผิดพลาดออกมา ตอนที่มันแปลโค้ดภาษาไพธอน ไปเป็นไบต์โค้ด. ข้อผิดพลาดเชิงวากยสัมพันธ์ จะบอกว่า โปรแกรมมีโครงสร้างของภาษาที่เขียนไม่ถูก ตัวอย่างเช่น การลืมเครื่องหมาย : ที่คำสั่ง def ก็จะทำให้ไพธอนอินเตอร์พรีเตอร์แจ้ง ข้อความ SyntaxError: invalid syntax ออกมา
  • ข้อผิดพลาดเวลาดำเนินการ (runtime errors) จะถูกไพธอนอินเตอร์พรีเตอร์แจ้งออกมา ตอนที่รันโปรแกรมแล้วมีปัญหาเกิดขึ้น ข้อความแจ้งข้อผิดพลาดเวลาดำเนินการส่วนใหญ่ จะบอกรายละเอียดเกี่ยวกับว่า ข้อผิดพลาดเกิดขึ้นที่ไหน ฟังก์ชันอะไรที่กำลังรันอยู่ตอนที่เกิดข้อผิดพลาด ตัวอย่างเช่น การเรียกตัวเอง (recursion) ที่เรียกต่อๆ ไปไม่มีที่สิ้นสุด สุดท้ายแล้ว ก็จะทำให้เกิดข้อผิดพลาดเวลาดำเนินการ RecursionError: maximum recursion depth exceeded ออกมา
  • ข้อผิดพลาดเชิงความหมาย (semantic errors) เป็นปัญหาที่โปรแกรมทำงานได้ โดยไม่มีข้อความแจ้งข้อผิดพลาดออกมาเลย แต่โปรแกรมทำงานไม่ถูกต้อง ตัวอย่างเช่น นิพจน์อาจจะไม่ได้ถูกประเมินตามลำดับที่เราคาด และให้ผลลัพธ์ที่ผิดออกมา

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

A.1 ข้อผิดพลาดเชิงวากยสัมพันธ์

ข้อผิดพลาดเชิงวากยสัมพันธ์ (syntax errors) มักจะแก้ได้ง่าย ถ้าเรารู้แล้วว่ามันคืออะไร แต่หลายครั้ง ข้อความแจ้งข้อผิดพลาดก็ไม่ได้บอกอะไรมาก ข้อความแจ้งข้อผิดพลาดที่พบบ่อยๆ คือ SyntaxError: invalid syntax และ SyntaxError: invalid token ซึ่งไม่ได้บอกอะไรมาก

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

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

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

รายการต่อไปนี้แสดงแนวทางปฎิบัติที่จะหลีกเลี่ยง ข้อผิดพลาดเชิงวากยสัมพันธ์ที่พบบ่อยๆ

  1. อย่าใช้คำสำคัญ (keyword) ของไพธอนในการตั้งชื่อตัวแปร
  2. ตรวจดูว่า มีเครื่องหมายจุดคู่ (colon) ที่ท้ายคำสั่งประกอบ เช่น คำสั่ง for คำสั่ง while คำสั่ง if และคำสั่ง def
  3. ตรวจสอบให้แน่ใจว่า สายอักขระทุกๆ อันในโปรแกรม ใช้เครื่องหมายคำพูดที่เข้ากัน (ถ้าเปิดด้วย ' ปิดด้วย '. ถ้าเปิดด้วย " ปิดด้วย ") ตรวจสอบว่าทุกๆ เครื่องหมายคำพูด เป็นเครื่องหมายแบบตรง ได้แก่ ' หรือ " ไม่ใช่แบบโค้ง ซึ่งได้แก่ หรือ
  4. ถ้ามีการใช้สายอักขระหลายบรรทัด ที่ใช้เครื่องหมายคำพูดสามครั้ง (ไม่ว่าจะเป็นแบบเดี่ยว ''' หรือแบบคู่ """) ตรวจสอบให้แน่ใจว่า สายอักขระหลายบรรทัดนั้นถูกปิดถูกต้องดีแล้ว สายอักขระที่ไม่ได้ถูกปิด (หรือปิดไม่ถูก) จะทำให้เกิดข้อผิดพลาด invalid token ออกมาที่ท้ายโปรแกรม หรือ ไพธอนอาจจะเหมารวมเอาส่วนของโปรแกรมที่ตามมาเป็นสายอักขระไปด้วย จนกว่ามันจะเจอสายอักขระใหม่ ในกรณีที่สองนี้ ไพธอนจะไม่ให้ข้อความแจ้งข้อผิดพลาดออกมาเลย
  5. ตัวดำเนินการที่เปิดแล้ว แต่ยังไม่ปิด ได้แก่ ตัวดำเนินการ ( หรือ ตัวดำเนินการ { หรือ ตัวดำเนินการ [ จะทำให้ไพธอนคิดว่าบรรทัดถัดไปเป็นส่วนหนึ่งของคำสั่งที่ยังไม่ปิด ส่วนใหญ่แล้ว ข้อความแจ้งข้อผิดพลาด จะออกมาที่บรรทัดถัดไป
  6. ตรวจสอบว่ามีการใช้ = แทน == ในการตรวจสอบเงื่อนไขหรือไม่ เช่น การตรวจสอบเงื่อนไข ของคำสั่ง if
  7. ตรวจสอบการย่อหน้า ให้แน่ใจว่ามันถูกต้อง ไพธอนสามารถรับได้ทั้ง ช่องว่าง (space) หรือ การตั้งระยะ (tab) แต่ถ้าเราใช้มันผสมกัน มันจะมีปัญหา วิธีที่ดีที่สุด ที่จะหลีกเลี่ยงปัญหาแบบนี้คือ ใช้โปรแกรมบรรณาธิกรข้อความ (text editor) ที่เหมาะกับภาษาไพธอน และสร้างการย่อหน้าด้วยอักขระแบบเดียวกัน (เช่น เมื่อเราพิมพ์การตั้งระยะ มันจะเปลี่ยนเป็น ช่องว่างสี่ช่องให้แทน)
  8. ถ้ามีอักขระที่ไม่ใช่รหัสแอสกี (non-ASCII characters) อยู่ในโค้ด (รวมถึง อยู่ในสายอักขระ หรือ อยู่ในส่วนข้อคิดเห็นด้วย) มันจะมีปัญหาได้ ถึงแม้ว่าไพธอน 3 ทั่วไปแล้ว จะสามารถรับอักขระที่ไม่ใช่รหัสแอสกีได้ แต่ให้ระวัง เวลาที่เราเอาข้อความมาจากเวปไซต์ หรือจากแหล่งอื่นๆ

ถ้าลองตรวจดูตามนี้แล้ว ก็ยังแก้ปัญหาไม่ได้ ลองดูหัวข้อถัดไป

A.1.1 ลองทำตั้งหลายอย่างแล้ว แต่ไม่เห็นมีอะไรเปลี่ยนแปลงเลย

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

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

มีตัวการเด่นๆ อยู่บ้าง เช่น

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

ถ้ายังติดอีก และก็ยังหาไม่เจอ ให้ลองเปิดไฟล์ขึ้นมาเขียนโปรแกรมใหม่เลย เช่น “Hello, World!” และลองให้เห็นว่าโปรแกรมนี้ถูกรันได้ แล้วค่อยๆ ใส่ส่วนต่างๆ ของโปรแกรมเดิมเข้าไปในโปรแกรมใหม่นี้

A.2 ข้อผิดพลาดเวลาดำเนินการ

ถ้าโปรแกรมมีวากยสัมพันธ์ที่ถูกต้อง ไพธอนจะอ่านมันได้ และอย่างน้อยก็กสามารถเริ่มรันมันได้ แล้วจะมีอะไรที่จะผิดได้อีก?

A.2.1 ไม่เห็นโปรแกรม มันทำอะไรเลย

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

แต่ถ้าเราไม่ได้อยากให้เป็นแบบนี้ ตรวจสอบว่ามีการเรียกใช้ฟังก์ชันในโปรแกรม และตรวจสอบดูลำดับของการรันคำสั่ง ว่า การเรียกฟังก์ชันนั้นจะถูกรัน (ดูเรื่อง “ลำดับการรันคำสั่ง” ข้างล่าง)

A.2.2 โปรแกรมค้าง

ถ้าโปรแกรมหยุด และดูเหมือนไม่ได้ทำอะไร มันเรียกว่าโปรแกรมแฮง (hang) ซึ่งบ่อยๆ เลย ที่โปรแกรมแฮง มาจากโปรแกรมติดอยู่ในลูปไม่สิ้นสุด (infinite loop) หรือติดอยู่ในการวนซ้ำไม่สิ้นสุด (infinite recursion)

  • ถ้ามีลูปที่สงสัยว่าอาจจะเป็นตัวสร้างปัญหา ให้ใส่คำสั่ง print เข้าไปก่อนเข้าลูป โดยให้พิมพ์ออกมาว่า “entering the loop” และอีกอันใส่เข้าไปหลังจากเพิ่งออกจากลูป พิมพ์ว่า “exiting the loop”

รันโปรแกรม แล้วถ้าเห็นแต่ข้อความแรก ไม่เห็นข้อความที่สอง เราเจอลูปไม่สิ้นสุดแล้ว ลองดูหัวข้อ “ลูปไม่สิ้นสุด” ข้างล่าง

  • ส่วนใหญ่ การวนซ้ำไม่สิ้นสุด จะทำให้โปรแกรนรันไปพักหนึ่ง ก่อนที่จะให้ “RuntimeError: Maximum recursion depth exceeded” ออกมา. ถ้าเป็นแบบนี้ ลองดูหัวข้้อ “การวนซ้ำไม่สิ้นสุด” ข้างล่าง.

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

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

ลูปไม่สิ้นสุด

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

ตัวอย่างเช่น:

while x > 0 and y < 0 :
    # do something to x
    # do something to y
 
    print('x: ', x)
    print('y: ', y)
    print("condition: ", (x > 0 and y < 0))

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

การวนซ้ำไม่สิ้นสุด

ส่วนใหญ่ การวนซ้ำไม่สิ้นสุด จะทำให้โปรแกรมรันไปได้พักหนึ่ง ก่อนจะให้ข้อผิดพลาด Maximum recursion depth exceeded ออกมา

ถ้าสงสัยว่า ฟังก์ชันเป็นตัวการทำให้เกิดการวนซ้ำไม่สิ้นสุด ลองดูให้แน่ใจว่า มีกรณีฐาน (base case) มันจะต้องมีเงื่อนไข ที่ให้ฟังก์ชันคืนค่าออกมาได้ โดยไม่ต้องเรียกการวนซ้ำอีก ถ้าไม่มี ลองทนทวนอัลกอริทึมใหม่ และกำหนดกรณีฐาน นี้

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

ลำดับขั้นตอนการทำงาน

ถ้าไม่แน่ใจว่าลำดับขั้นตอนการทำงานของโปรแกรมเป็นอย่างไร ลองใส่คำสั่ง print เข้าไปตอนเริ่มของแต่ละฟังก์ชัน โดยพิมพ์ข้อความ เช่น “entering function foo” เมื่อ foo เป็นชื่อของฟังก์ชัน

ตอนนี้ถ้ารันโปรแกรม มันจะพิมพ์ข้อความต่างๆ ซึ่งเหมือนร่องรอยบอกการเรียกใช้ของแต่ละฟังก์ชัน

A.2.3 เวลารันโปรแกรมแล้วได้เอ็กเซ็ปชั่นมา

ถ้ามีอะไรผิดพลาดระหว่างการรันโปรแกรม ไพธอนจะพิมพ์ข้อความ รวมถึงชื่อของเอ็กเซ็ปชั่น และบรรทัดที่โปรแกรมเจอปัญหา และการสืบย้อน (traceback) ออกมา

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

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

NameError:

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

TypeError:

อาจเกิดได้จากหลายสาเหตุ:

  • อาจเกิดจากการพยายามไปใช้ค่าแบบไม่ถูกต้อง ตัวอย่างเช่น การอ้างดัชนีของ สายอักขระ ลิสต์ หรือ ทูเพิล ด้วยค่าอื่นที่ไม่ใช่เลขจำนวนเต็ม
  • อาจเกิดจากสายอักขระจัดรูปแบบ กับตัวแปรที่ผ่านเข้าไปจัดรูปแบบ ไม่เข้ากัน อาจเป็นกรณีจำนวนไม่เท่ากัน หรืออาจเป็นชนิดของค่าไม่ตรงกัน ตัวอย่างเช่น print("%d, %d"%(5, 6, 7)) และ print("%d"%'5')
  • อาจเกิดจากจำนวนอาร์กิวเมนต์ที่ส่งให้ฟังก์ชัน ไม่ตรงตามจำนวนอาร์กิวเมนต์ที่ฟังก์ชันกำหนด สำหรับเมธอด ตรวจสอบคำนิยามของเมธอด และตรวจสอบว่าพารามิเตอร์แรก เป็น self แล้วตรวจดูการเรียกใช้เมธอด ว่าเรียกใช้เมธอดจากออบเจ๊คต์ถูกชนิด และส่งอาร์กิวเมนต์อื่นๆ ไปให้ถูก
KeyError:

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

AttributeError:

อาจเกิดจากการพยายามอ้างถึงลักษณะประจำ หรือเมธอด ที่ไม่มีอยู่ ลองตรวจสอบการสะกด เราสามารถใช้ฟังก์ชันสำเร็จ dir หรือ vars เพื่อตรวจดูลักษณะประจำต่างๆ ที่มีอยู่ได้

ถ้า AttributeError บอกว่า ออบเจ๊คต์มีNoneType นั่นหมายถึงว่า ตัวออบเจ๊คต์มีค่าเป็น None ดังนั้นปัญหาไม่ใช่ชื่อของลักษณะประจำ แต่เป็นตัวออบเจ๊คต์เอง

สาเหตุที่ออบเจ๊คต์เป็น None อาจจะมาจาก เราลืม return ค่าออกมาจากฟังก์ชัน อย่างเช่น ถ้ามีวิธีที่จะรันฟังก์ชันจนจบได้ โดยไม่ต้องทำคำสั่ง return ฟังก์ชันจะให้ค่า None ออกมาโดยอัตโนมัติ อีกสาเหตุที่พบบ่อยๆ ก็คือ การใช้ผลลัพธ์จากเมธอดของลิสต์ เช่น sort ที่ให้ค่าออกมาเป็น None

IndexError:

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

ไพธอนดีบักเกอร์ (pdb) มีประโยชน์มากในการช่วยสืบหาสาเหตุของเอ็กเซ็ปชั่น เพราะว่า มันจะช่วยให้เราสามารถดูสถานะของโปรแกรมได้ทันทีก่อนข้อผิดพลาดจะเกิดขึ้น ศึกษาเรื่อง pdb เพิ่มเติมได้จาก https://docs.python.org/3/library/pdb.html

A.2.4 เราใส่คำสั่ง print เข้าไปเยอะ จนเราเริ่มมึนและท่วมท้นจากเอาต์พุตที่เห็น

ปัญหาหนึ่งจากการใช้คำสั่ง print เพื่อดีบัก คือสุดท้ายเราอาจจะท่วมท้นกับเอาต์พุตจำนวนมากที่ได้ มีสองวิธี คือ สะสางเอาต์พุตให้ดูง่ายขึ้น หรือสะสางโปรแกรมให้เรียบง่ายขึ้น

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

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

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

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

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

A.3 ข้อผิดพลาดเชิงความหมาย

จากหลายๆ แง่มุม ข้อผิดพลาดเชิงความหมาย (semantic errors) เป็นเรื่องที่ดีบักยากที่สุด เพราะว่า ไพธอนอินเตอร์พรีเตอร์ จะไม่บอกว่ามีอะไรผิดเลย มีแต่เราเท่านั้นที่รู้ว่าโปรแกรมควรจะทำงานอย่างไร

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

บางที เราก็อยากจะให้โปรแกรมรันช้าลงพอที่จะเห็นการทำงานได้ และดีบักเกอร์ก็ช่วยทำให้โปรแกรมรันช้าลงได้ เพียงแต่ การใช้คำสั่ง print มันจะสะดวกกว่าการใช้ดีบักเกอร์ ที่รวมการใส่จุดหยุด (breakpoint) และการรันทีละขั้น (single stepping) ไปจนพบตำแหน่งของปัญหา

A.3.1 โปรแกรมไม่ทำงาน

เราควรจะลองถามตัวเองดูว่า:

  • มีอะไรไหมที่โปรแกรมควรจะทำ แต่ดูเหมือนมันไม่ได้ทำ? ลองหาส่วนของโปรแกรม ที่ทำหน้าที่นั้น และตรวจสอบว่ามันถูกรันตอนที่เราคิดว่ามันควรจะถูกรัน
  • มีอะไรที่เกิดขึ้น ทั้งๆ ที่มันไม่ควรจะเกิดหรือไม่? ลองหาส่วนของโปรแกรมที่ทำหน้าที่นั้น และตรวจดูว่ามันถูกรันตอนที่ไม่ควรจะถูกรันหรือไม่
  • มีส่วนของโปรแกรมที่ให้ผลลัพธ์ออกมา ต่างจากที่คิดหรือไม่? ตรวจสอบว่า เราเข้าใจโปรแกรมที่มีปัญหาดีแล้ว โดยเฉพาะถ้ามันเรียกใช้ฟังก์ชัน หรือเมธอดจากไพธอนมอดูลอื่นๆ อ่านเอกสารที่เกี่ยวข้องกับฟังก์ชันที่เรียกใช้ ลองใช้ฟังก์ชันต่างๆ ดูก่อน กับโปรแกรมง่ายๆ ในสถานการณ์ง่ายๆ

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

วิธีดีที่สุดในการปรับแนวคิดในหัว คือ แตกโปรแกรมออกเป็นส่วนย่อยๆ (โดยทั่วไป คือ แตกเป็นฟังก์ชัน และเมธอดต่างๆ) แล้วทดสอบการทำงานของแต่ละส่วนอย่างอิสระ ถ้าเราเจอว่าอะไรที่แนวคิดในหัวกับความเป็นจริงไม่ตรงกัน เราก็จะเจอคำตอบของปัญหา

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

A.3.2 มีนิพจน์ที่ใหญ่แล้วก็ดูยากมาก แล้วมันก็ไม่ทำงานแบบที่คิด

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

ตัวอย่างเช่น:

self.hands[i].addCard(self.hands[self.findNeighbor(i)].popCard())

อันนี้อาจจะเขียนใหม่เป็น:

neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)

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

ปัญหาอีกอย่าง ที่อาจเกิดกับนิพจน์ที่ซับซ้อน คือ ลำดับของการคำนวณอาจจะตรงตามที่เราคิด ตัวอย่างเช่น ถ้าเราตีความนิพจน์ $\frac{x}{2 \pi}$ เป็นโปรแกรมไพธอน เราอาจจะเขียน:

y = x / 2 * math.pi

ซึ่ง มันไม่ถูก เพราะว่า การคูณและการหาร มีลำดับการทำก่อนหลังเท่ากัน และจะถูกทำจากซ้ายไปขวา ดังนั้นนิพจน์ของโปรแกรมไพธอนนี้ คือ $x \pi / 2$

วิธีแก้ปัญหาที่ดี คือ การใส่วงเล็บเข้าไป เพื่อกำหนดลำดับการทำให้ชัดเจน:

 y = x / (2 * math.pi)

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

A.3.3 มีฟังก์ชันที่มันให้ค่าออกมาไม่เหมือนที่คิด

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

return self.hands[i].removeMatches()

เราอาจจะเขียนโปรแกรมเป็น:

count = self.hands[i].removeMatches()
return count

ตอนนี้ เราสามารถพิมพ์ค่า count ออกมาดูก่อนได้ง่ายขึ้น

A.3.4 ติด ช่วยหน่อย

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

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

ถ้ารู้สึกตัวเองว่ากำลังมีอาการเหล่านี้ พักและออกไปเดินเล่นก่อน พอสงบแล้ว ค่อยกลับคิดโปรแกรมต่อ โปรแกรมมันทำอะไร? อะไรที่มันจะทำให้เกิดพฤติกรรมแบบนั้นได้? ตอนไหนที่โปรแกรมมันทำงานได้ และเราทำอะไรไปหลังจากนั้น?

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

A.3.5 ไม่ได้จริงๆ มาช่วยดูให้หน่อย

บางทีมันก็แก้เองไม่ได้จริงๆ แม้แต่นักเขียนโปรแกรมที่เก่งที่สุด บางครั้งก็ติดเหมือนกัน บางครั้ง เราอยู่กับโปรแกรมงานเกินไป จนทำให้เรามองไม่เห็นข้อผิดพลาด เราต้องการสายตาสดๆ คู่ใหม่

ก่อนที่เราจะไปตามคนมาช่วย ให้เตรียมตัวก่อน โปรแกรมเราอาจจะเรียบง่ายที่สุด และเราก็ควรจะลองกับอินพุตที่เล็กที่สุด แล้วที่จะเห็นข้อผิดพลาด เราควรจะวางคำสั่ง print ไว้ที่ที่เหมาะสมเรียบร้อยแล้ว (และเอาต์พุตก็ควรจะเข้าใจได้ง่าย) เราควรจะต้องเข้าใจโปรแกรมดีพอที่จะอธิบายได้อย่างชัดเจน และกระชับ

เวลาที่เอาคนมาช่วย ให้แน่ใจว่า ให้ข้อมูลต่างๆ ที่เขาต้องการแล้ว:

  • ถ้ามีข้อความแจ้งข้อผิดพลาด มันบอกว่าอะไร และส่วนไหนของโปรแกรมที่มันแจ้ง?
  • เราเพิ่งทำอะไรไป ก่อนที่จะเกิดปัญหาขึ้น? บรรทัดไหนบ้างของโปรแกรมที่เพิ่งเขียนเข้าไป หรือกรณีทดสอบไหนที่เพิ่งใส่เข้าไปแล้วโปรแกรมไม่ผ่าน?
  • เราได้ลองอะไรไปแล้วบ้าง และเรารู้อะไรแล้วบ้าง?

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

จำไว้ว่า เป้าหมาย คือ ไม่ใช่แค่ให้โปรแกรมทำงานได้ เป้าหมาย คือ เรียนรู้ว่าจะทำอย่างไรให้โปรแกรมทำงานได้

https://greenteapress.com/thinkpython2/html/thinkpython2021.html

python/debugging.txt · Last modified: 2021/08/30 09:55 (external edit)

Page Tools