Example 4 — Find the Biggest Sensor Jump (debugging) ตัวอย่างที่ 4 — หาจุดที่ค่าเซนเซอร์เปลี่ยนมากที่สุด (debugging)
A debugging problem: the program below should tell which sensor reading changed the most from the one before, but it hides one bug. โจทย์ debugging: โปรแกรมด้านล่างควรบอกว่าค่าที่อ่านได้ครั้งไหนเปลี่ยนจากครั้งก่อนมากที่สุด แต่มี bug ซ่อนอยู่ 1 จุด
How to work on this page: read the problem and the flowcharts, then check the code against the flowcharts box by box. Run the test list, find the wrong line, and fix the program in the code box yourself. Run the test list again until every test gives its expected output, and only then open the solution. To review the kinds of error, the kinds of test case and the bug-hunting tools, see 11 Testing and Debugging.
วิธีทำโจทย์หน้านี้: อ่านโจทย์และ flowchart แล้วตรวจโค้ดเทียบกับ flowchart ทีละกล่อง จากนั้นรันตามรายการทดสอบ หาบรรทัดที่ผิด และแก้โปรแกรมในกล่องโค้ดเอง รันรายการทดสอบซ้ำจนทุกข้อได้ผลตามที่ควรได้ แล้วจึงเปิดดูเฉลย ทบทวนชนิดของ error, ชนิดของ test case และเครื่องมือหา bug ได้ที่บท 11 การทดสอบและการแก้ไขโปรแกรม
A sensor is read once every minute. The user types the readings as non-negative whole numbers separated by commas (at least 2 readings). The program must tell which reading changed the most from the reading just before it. The first reading is reading 1. A change counts in both directions, up or down (use the size of the difference and ignore its sign). If several readings share the biggest change, answer the first of them.
เซนเซอร์วัดค่าทุก 1 นาที ผู้ใช้ป้อนค่าที่อ่านได้เป็นจำนวนเต็มไม่ติดลบ คั่นด้วยจุลภาค (,) อย่างน้อย 2 ค่า โปรแกรมต้องบอกว่าค่าที่อ่านได้ครั้งที่เท่าไรเปลี่ยนจากครั้งก่อนหน้ามากที่สุด โดยนับครั้งแรกเป็นครั้งที่ 1 นับการเปลี่ยนทั้งขาขึ้นและขาลง (ใช้ขนาดของผลต่าง ไม่คิดเครื่องหมาย) และถ้ามีหลายครั้งที่เปลี่ยนมากที่สุดเท่ากัน ให้ตอบครั้งแรก
- Example: 12,15,9,10 → 3 (reading 3 drops from 15 to 9, a change of 6)
- to_numbers(line) — turns the typed text into a list of whole numbers
- biggest_jump_at(readings) — returns the number of the reading with the biggest change
- ตัวอย่าง: 12,15,9,10 → 3 (ครั้งที่ 3 ลดจาก 15 เป็น 9 เปลี่ยนไป 6)
- to_numbers(line) — แปลงข้อความที่ป้อนเป็นลิสต์ของจำนวนเต็ม
- biggest_jump_at(readings) — คืนค่าครั้งที่ของค่าที่อ่านได้ซึ่งเปลี่ยนมากที่สุด
New in this program: split(",") cuts text at every comma and gives a list of the pieces; int() turns text such as "15" into the whole number 15; range(1, len(readings)) starts counting at 1, so every reading it visits has a reading before it at i - 1.
คำสั่งใหม่ในโปรแกรมนี้: split(",") ตัดข้อความตรงทุกจุลภาค แล้วได้ลิสต์ของข้อความแต่ละช่วง int() แปลงข้อความ เช่น "15" เป็นจำนวนเต็ม 15 และ range(1, len(readings)) เริ่มนับที่ 1 ค่าที่อ่านได้ทุกครั้งที่ loop ผ่านจึงมีครั้งก่อนหน้าอยู่ที่ i - 1 เสมอ
The program starts by reading the typed readings into line. Next it turns the text into a list of numbers with the sub-function to_numbers and stores it in readings. Then it finds the reading with the biggest change with the sub-function biggest_jump_at, shows the result with print, and the program ends.
โปรแกรมนี้เริ่มต้นด้วยรับค่าที่อ่านได้ที่ผู้ใช้ป้อนเก็บใน line ถัดไปแปลงข้อความเป็นลิสต์ของตัวเลขด้วย function ย่อยชื่อ to_numbers แล้วเก็บใน readings หลังจากนั้นหาครั้งที่เปลี่ยนมากที่สุดด้วย function ย่อยชื่อ biggest_jump_at แล้วแสดงผลด้วย print และจบการทำงานของโปรแกรม
Readings: 12,15,9,10 2
Try it yourself — type readings separated by commas and press Submit. They replace the [input] box and run the code that is in the box right now (including your fix).
ลองป้อนค่าเอง — พิมพ์ค่าที่อ่านได้คั่นด้วยจุลภาคแล้วกดส่ง ค่าจะแทนที่กล่อง [input] แล้วรันโค้ดที่อยู่ในกล่องตอนนี้ (รวมถึงส่วนที่คุณแก้แล้ว)
to_numbers starts by cutting line at every comma with split and storing the pieces in parts. It creates an empty list numbers and sets i = 0 as the position of the piece. It checks whether i < len(parts); if it is, it converts the piece with int, adds it to numbers with append, increases i by 1 and checks again. This repeats until the condition is false, so it sends numbers back with return.
to_numbers เริ่มต้นด้วยตัด line ตรงทุกจุลภาคด้วย split แล้วเก็บแต่ละช่วงไว้ใน parts สร้างลิสต์ว่างชื่อ numbers และกำหนดค่า i = 0 เป็นตัวชี้ตำแหน่งของช่วง แล้วตรวจสอบเงื่อนไขว่า i < len(parts) หรือไม่ ถ้าใช่ให้แปลงช่วงนั้นด้วย int แล้วเพิ่มเข้า numbers ด้วย append เพิ่ม i ขึ้น 1 และกลับไปตรวจสอบอีกครั้ง ทำแบบนี้จนเงื่อนไขเป็นเท็จ จึงส่งค่า numbers กลับด้วย return
Test the function on its own: it should give a list of whole numbers.
ทดสอบฟังก์ชันนี้แยกเดี่ยวๆ ควรได้ลิสต์ของจำนวนเต็ม
[12, 15, 9, 10] [40, 25]
biggest_jump_at starts by setting best_step = 0 to hold the answer and best_jump = -1 as the biggest change found so far (smaller than any real change, so even a change of 0 is recorded). It sets i = 1 as the position of the reading and checks whether i < len(readings). If it is, it finds the change from the previous reading with jump = readings[i] - readings[i - 1], then checks whether jump > best_jump. If it is, it stores best_jump = jump and best_step = i + 1 (the reading number, counted from 1). Either way it increases i by 1 and goes back to check i < len(readings) again. This repeats until the condition is false, so it sends best_step back with return.
biggest_jump_at เริ่มต้นด้วยกำหนดค่า best_step = 0 สำหรับเก็บคำตอบ และ best_jump = -1 เป็นค่าการเปลี่ยนที่มากที่สุดที่พบจนถึงตอนนี้ (น้อยกว่าการเปลี่ยนจริงทุกค่า การเปลี่ยนเป็น 0 จึงยังถูกบันทึก) กำหนดค่า i = 1 เป็นตัวชี้ตำแหน่งของค่าที่อ่านได้ แล้วตรวจสอบเงื่อนไขว่า i < len(readings) หรือไม่ ถ้าใช่ให้หาการเปลี่ยนจากครั้งก่อนหน้าด้วย jump = readings[i] - readings[i - 1] แล้วตรวจสอบว่า jump > best_jump หรือไม่ ถ้าใช่ให้เก็บ best_jump = jump และ best_step = i + 1 (ครั้งที่ของค่าที่อ่านได้ ซึ่งนับจาก 1) ไม่ว่าทางใดทางหนึ่ง หลังจากนั้นเพิ่ม i ขึ้น 1 แล้วกลับไปตรวจสอบ i < len(readings) อีกครั้ง ทำแบบนี้จนเงื่อนไขเป็นเท็จ จึงส่งค่า best_step กลับด้วย return
Test the function on its own with lists of numbers (no typing needed).
ทดสอบฟังก์ชันนี้แยกเดี่ยวๆ ด้วยลิสต์ของตัวเลข (ไม่ต้องพิมพ์ค่า)
2 3
Run each input yourself (type it into the form in card 2) and compare the result with the expected output column. The last column shows what the code with the bug gives.
รันทีละข้อด้วยตัวเอง (พิมพ์ลงในช่องของการ์ดที่ 2) แล้วเทียบผลกับคอลัมน์ผลที่ควรได้ คอลัมน์สุดท้ายคือผลจากโค้ดที่ยังมี bug
| Inputข้อมูลที่ป้อน | Kindชนิด | Expected outputผลที่ควรได้ | Output of the buggy codeผลจากโค้ดที่มี bug |
|---|---|---|---|
| 12,15,9,10 | Normal | 3 | 2 ✗ |
| 5,7,12,13 | Normal (rising only)Normal (ขึ้นอย่างเดียว) | 3 | 3 ✓ |
| 30,5,6,8 | Boundary (biggest change at reading 2)Boundary (เปลี่ยนมากสุดที่ครั้งที่ 2) | 2 | 4 ✗ |
| 3,4,5,20 | Boundary (biggest change at the last reading)Boundary (เปลี่ยนมากสุดที่ครั้งสุดท้าย) | 4 | 4 ✓ |
| 40,40 | Boundary (2 readings, no change)Boundary (2 ค่า ไม่เปลี่ยน) | 2 | 2 ✓ |
| 40,25 | Boundary (2 readings, a drop)Boundary (2 ค่า ลดลง) | 2 | 0 ✗ |
| 10,14,10 | Boundary (equal changes → the first one)Boundary (เปลี่ยนเท่ากัน → ตอบครั้งแรก) | 2 | 2 ✓ |
| 12,abc,15 | Error (not a number)Error (ไม่ใช่ตัวเลข) | should be rejected, but this program does not check its input ควรถูกปฏิเสธ แต่โปรแกรมนี้ไม่ได้ตรวจข้อมูลที่รับเข้ามา | ValueError: invalid literal for int() with base 10: 'abc' |
- Check the code of both functions and the main program against their flowcharts, box by box.
- Run every input in the test list and note whether the actual output matches the expected output.
- Why does 5,7,12,13 pass while 12,15,9,10 fails? What do all the failing inputs have in common?
- Which kind of error is this bug, and why?
- Add print(i, jump, best_jump) inside the loop of biggest_jump_at and fill in the trace table below for 12,15,9,10.
- Fix the code in the box and run the test list again. Every Normal and Boundary input must give its expected output.
- Think further: why does the buggy code answer 0 for 40,25, when there is no reading 0? And how should the program deal with 12,abc,15?
- ตรวจโค้ดของทั้ง 2 ฟังก์ชันและโปรแกรมหลักเทียบกับ flowchart ทีละกล่อง
- รันทุกข้อในรายการทดสอบ แล้วจดผลที่ได้จริงว่าตรงกับผลที่ควรได้หรือไม่
- ทำไม 5,7,12,13 ผ่าน แต่ 12,15,9,10 ไม่ผ่าน ข้อที่ไม่ผ่านทุกข้อมีอะไรเหมือนกัน
- bug นี้เป็น error ชนิดไหน เพราะอะไร
- ใส่ print(i, jump, best_jump) ใน loop ของ biggest_jump_at แล้วกรอก trace table ด้านล่างของ 12,15,9,10
- แก้โค้ดในกล่อง แล้วรันรายการทดสอบซ้ำ ข้อที่เป็น Normal และ Boundary ต้องได้ผลตามตารางทุกข้อ
- ชวนคิดต่อ: ทำไมโค้ดที่มี bug ตอบ 0 กับ 40,25 ทั้งที่ไม่มีค่าที่อ่านได้ครั้งที่ 0 และโปรแกรมควรจัดการกับ 12,abc,15 อย่างไร
| i | jump | jump > best_jump? | best_jump | best_step |
|---|---|---|---|---|
| 1 | ||||
| 2 | ||||
| 3 |
Show the fixed code ดูโค้ดที่แก้แล้ว
The bug is on the line jump = readings[i] - readings[i - 1]. A drop gives a negative jump, so it is never bigger than best_jump and drops are ignored. The problem asks for the size of the change, so use the absolute value: abs() gives a number without its sign, e.g. abs(-6) is 6. Change the line to jump = abs(readings[i] - readings[i - 1]). It is a logic error: the program runs to the end with no error message. Inputs that only rise hide the bug, because every change is already positive. With 40,25 the only change is negative, so best_step keeps its starting value 0.
bug อยู่ที่บรรทัด jump = readings[i] - readings[i - 1] เมื่อค่าลดลง jump จะติดลบ จึงไม่มีทางมากกว่า best_jump การเปลี่ยนขาลงจึงถูกมองข้ามทั้งหมด โจทย์ต้องการขนาดของการเปลี่ยน จึงต้องใช้ค่าสัมบูรณ์ abs() ให้ค่าตัวเลขโดยไม่คิดเครื่องหมาย เช่น abs(-6) ได้ 6 แก้บรรทัดเป็น jump = abs(readings[i] - readings[i - 1]) เป็น logic error เพราะโปรแกรมรันจบโดยไม่มีข้อความ error ส่วนข้อมูลที่มีแต่ขาขึ้นซ่อน bug ไว้ เพราะการเปลี่ยนทุกครั้งเป็นบวกอยู่แล้ว และ 40,25 มีการเปลี่ยนครั้งเดียวซึ่งติดลบ best_step จึงยังเป็นค่าเริ่มต้น 0
| i | jump | jump > best_jump? | best_jump | best_step |
|---|---|---|---|---|
| 1 | 3 | ✓ | 3 | 2 |
| 2 | -6 | ✗ | 3 | 2 |
| 3 | 1 | ✗ | 3 | 2 |
Readings: 12,15,9,10 3
The Run buttons on this page simulate Python with JavaScript, the same as the other lessons. To run the real Python, paste the code into Google Colab.
ปุ่ม Run ในหน้านี้จำลองโค้ด Python ด้วย JavaScript เหมือนบทเรียนอื่นๆ หากต้องการรัน Python จริง ให้คัดลอกโค้ดไปวางที่ Google Colab
References References / แหล่งอ้างอิง
- Python docs — str.split (cutting the typed text at each comma) (ตัดข้อความที่ป้อนตรงทุกจุลภาค)
- Python docs — Built-in Functions: abs andและ int (absolute value in the fix; text to whole number) (ค่าสัมบูรณ์ในเฉลย และการแปลงข้อความเป็นจำนวนเต็ม)
- Kinds of error, kinds of test case and bug-hunting tools: 11 Testing and Debugging; flowchart symbols: 01 Basic → Flowchart Basic. ชนิดของ error, ชนิดของ test case และเครื่องมือหา bug: 11 การทดสอบและการแก้ไขโปรแกรม ; สัญลักษณ์ของ flowchart: 01 Basic → Flowchart Basic