Example 5 — Find the Vote Winner (debugging) ตัวอย่างที่ 5 — หาผู้ชนะการโหวต (debugging)
A debugging problem: the program below should count the votes and name the winner, 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 club is choosing its president. Each candidate has a code of one capital letter (A to E), and each ballot carries one code. The vote counter types the codes of all the ballots as one piece of text with no spaces (at least 1 ballot). The program must show the winner and the number of votes the winner got. If several candidates share the most votes, the winner is the one whose code appears first in the typed text.
ชมรมแห่งหนึ่งกำลังเลือกประธาน ผู้สมัครแต่ละคนมีรหัสเป็นอักษรตัวใหญ่ 1 ตัว (A ถึง E) และบัตรโหวต 1 ใบมีรหัส 1 ตัว ผู้นับคะแนนพิมพ์รหัสจากบัตรโหวตทุกใบต่อกันเป็นข้อความเดียวโดยไม่เว้นวรรค (อย่างน้อย 1 ใบ) โปรแกรมต้องแสดงผู้ชนะและจำนวนคะแนนโหวตที่ผู้ชนะได้ ถ้ามีผู้สมัครหลายคนได้คะแนนสูงสุดเท่ากัน ผู้ชนะคือคนที่รหัสปรากฏในข้อความที่พิมพ์ก่อน
- Example: BACBB → Winner: B and Votes: 3
- count_votes(ballots) — returns a dictionary that holds how many votes each candidate got
- find_winner(tally) — returns the code of the winner
- ตัวอย่าง: BACBB → Winner: B และ Votes: 3
- count_votes(ballots) — คืน dictionary ที่เก็บว่าผู้สมัครแต่ละคนได้คะแนนโหวตกี่คะแนน
- find_winner(tally) — คืนรหัสของผู้ชนะ
New in this program: tally = {} creates an empty dictionary; vote not in tally is true when vote is not a key of tally yet; tally[vote] = 0 adds the key vote with the value 0; tally[vote] += 1 adds 1 to the value of that key. for name in tally visits the keys in the order they were added, which is the order in which the codes first appear in the text. Because find_winner uses >, a later candidate with the same number of votes does not replace the one found first (with >= it would).
คำสั่งใหม่ในโปรแกรมนี้: tally = {} สร้าง dictionary ว่าง vote not in tally เป็นจริงเมื่อ vote ยังไม่เป็น key ของ tally ส่วน tally[vote] = 0 เพิ่ม key vote ที่มี value เป็น 0 และ tally[vote] += 1 เพิ่ม value ของ key นั้นขึ้น 1 คำสั่ง for name in tally ไล่ key ตามลำดับที่ถูกเพิ่มเข้าไป ซึ่งก็คือลำดับที่รหัสปรากฏครั้งแรกในข้อความ และเพราะ find_winner ใช้ > ผู้สมัครที่มาทีหลังแต่ได้คะแนนเท่ากัน จึงไม่แทนที่คนที่พบก่อน (ถ้าใช้ >= จะแทนที่)
The program starts by reading the typed codes into ballots. Next it counts the votes with the sub-function count_votes and stores the dictionary in tally. Then it finds the winner with the sub-function find_winner and stores it in winner. It shows the winner and the winner's votes, tally[winner], with print, and the program ends.
โปรแกรมนี้เริ่มต้นด้วยรับรหัสที่ผู้ใช้พิมพ์เก็บใน ballots ถัดไปนับคะแนนโหวตด้วย function ย่อยชื่อ count_votes แล้วเก็บ dictionary ที่ได้ใน tally หลังจากนั้นหาผู้ชนะด้วย function ย่อยชื่อ find_winner แล้วเก็บใน winner แสดงผู้ชนะและคะแนนโหวตของผู้ชนะ tally[winner] ด้วย print และจบการทำงานของโปรแกรม
Enter the ballots: BACBB Winner: B Votes: 1
Try it yourself — type the ballot codes with no spaces and press Submit. They replace the [input] box and run the code that is in the box right now (including your fix). Press Submit with an empty box to test an empty input.
ลองป้อนค่าเอง — พิมพ์รหัสจากบัตรโหวตต่อกันโดยไม่เว้นวรรคแล้วกดส่ง ค่าจะแทนที่กล่อง [input] แล้วรันโค้ดที่อยู่ในกล่องตอนนี้ (รวมถึงส่วนที่คุณแก้แล้ว) ถ้าต้องการทดสอบข้อมูลว่าง ให้กดส่งโดยไม่พิมพ์อะไร
count_votes starts by creating an empty dictionary tally. It checks whether there is another ballot in ballots; if there is, it takes its code as vote and checks whether vote not in tally. If it is, it adds the candidate with tally[vote] = 0, then adds one vote with tally[vote] += 1, and goes back to check for another ballot. If it is not, it goes straight back to check for another ballot. This repeats until there are no ballots left, so it sends tally back with return.
count_votes เริ่มต้นด้วยสร้าง dictionary ว่างชื่อ tally แล้วตรวจสอบว่ายังมีบัตรโหวตใน ballots เหลืออยู่หรือไม่ ถ้าใช่ให้นำรหัสของบัตรใบนั้นเป็น vote แล้วตรวจสอบเงื่อนไขว่า vote not in tally หรือไม่ ถ้าใช่ให้เพิ่มผู้สมัครด้วย tally[vote] = 0 แล้วเพิ่มคะแนน 1 คะแนนด้วย tally[vote] += 1 และกลับไปตรวจสอบว่ายังมีบัตรโหวตเหลือหรือไม่ ถ้าไม่ใช่ให้กลับไปตรวจสอบทันที ทำแบบนี้จนไม่มีบัตรโหวตเหลือ จึงส่งค่า tally กลับด้วย return
Test the function on its own: it should give a dictionary of votes.
ทดสอบฟังก์ชันนี้แยกเดี่ยวๆ ควรได้ dictionary ของคะแนนโหวต
{'B': 1, 'A': 1, 'C': 1}
{'D': 1}
find_winner starts by setting winner = "" to hold the answer and most = 0 as the most votes found so far. It checks whether there is another candidate in tally; if there is, it takes its code as name and checks whether tally[name] > most. If it is, it stores winner = name and most = tally[name]. Either way it goes back to check for another candidate. This repeats until there are no candidates left, so it sends winner back with return.
find_winner เริ่มต้นด้วยกำหนดค่า winner = "" สำหรับเก็บคำตอบ และ most = 0 เป็นคะแนนโหวตที่มากที่สุดที่พบจนถึงตอนนี้ แล้วตรวจสอบว่ายังมีผู้สมัครใน tally เหลืออยู่หรือไม่ ถ้าใช่ให้นำรหัสของผู้สมัครคนนั้นเป็น name แล้วตรวจสอบเงื่อนไขว่า tally[name] > most หรือไม่ ถ้าใช่ให้เก็บ winner = name และ most = tally[name] ไม่ว่าทางใดทางหนึ่ง หลังจากนั้นกลับไปตรวจสอบว่ายังมีผู้สมัครเหลือหรือไม่ ทำแบบนี้จนไม่มีผู้สมัครเหลือ จึงส่งค่า winner กลับด้วย return
Test the function on its own with dictionaries of votes (no typing needed).
ทดสอบฟังก์ชันนี้แยกเดี่ยวๆ ด้วย dictionary ของคะแนนโหวต (ไม่ต้องพิมพ์ค่า)
B E
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 |
|---|---|---|---|
| BACBB | Normal | Winner: B Votes: 3 |
Winner: B Votes: 1 ✗ |
| CABAD | Normal | Winner: A Votes: 2 |
Winner: C Votes: 1 ✗ |
| D | Boundary (1 ballot)Boundary (บัตรโหวต 1 ใบ) | Winner: D Votes: 1 |
Winner: D Votes: 1 ✓ |
| EEEE | Boundary (every ballot for one candidate)Boundary (ทุกใบเลือกผู้สมัครคนเดียวกัน) | Winner: E Votes: 4 |
Winner: E Votes: 1 ✗ |
| ABCDE | Boundary (every ballot different)Boundary (ทุกใบเลือกคนละคน) | Winner: A Votes: 1 |
Winner: A Votes: 1 ✓ |
| ABBA | Boundary (a tie → the one that appears first)Boundary (คะแนนเท่ากัน → ตอบคนที่ปรากฏก่อน) | Winner: A Votes: 2 |
Winner: A Votes: 1 ✗ |
| (empty)(ว่าง) | Error (no ballots)Error (ไม่มีบัตรโหวต) | should be rejected, but this program does not check its input ควรถูกปฏิเสธ แต่โปรแกรมนี้ไม่ได้ตรวจข้อมูลที่รับเข้ามา | Winner: KeyError: '' |
- Check the code of both functions and the main program against their flowcharts, box by box, and check each flowchart against the problem.
- Run every input in the test list and note whether the actual output matches the expected output.
- Why do D and ABCDE pass? What do you notice about Votes: in every failing test, and about the winner the buggy code picks?
- Which kind of error is this bug, and why?
- Add print(vote, tally) as the last line inside the loop of count_votes (lined up with if) and fill in the trace table below for BAB.
- Fix the code in the box and run the test list again. Every Normal and Boundary input must give its expected output.
- Think further: how could find_winner be changed to show every candidate who shares the most votes? And how should the program deal with an empty input?
- ตรวจโค้ดของทั้ง 2 ฟังก์ชันและโปรแกรมหลักเทียบกับ flowchart ทีละกล่อง และตรวจ flowchart แต่ละอันเทียบกับโจทย์
- รันทุกข้อในรายการทดสอบ แล้วจดผลที่ได้จริงว่าตรงกับผลที่ควรได้หรือไม่
- ทำไม D และ ABCDE ผ่าน ข้อที่ไม่ผ่านทุกข้อมีอะไรเหมือนกันที่ Votes: และผู้ชนะที่โค้ดที่มี bug เลือกเป็นใคร
- bug นี้เป็น error ชนิดไหน เพราะอะไร
- ใส่ print(vote, tally) เป็นบรรทัดสุดท้ายใน loop ของ count_votes (ย่อหน้าตรงกับ if) แล้วกรอก trace table ด้านล่างของ BAB
- แก้โค้ดในกล่อง แล้วรันรายการทดสอบซ้ำ ข้อที่เป็น Normal และ Boundary ต้องได้ผลตามตารางทุกข้อ
- ชวนคิดต่อ: ถ้าต้องการให้แสดงทุกคนที่ได้คะแนนสูงสุดเท่ากัน จะแก้ find_winner อย่างไร และโปรแกรมควรจัดการกับข้อมูลว่างอย่างไร
| Roundรอบ | vote | vote not in tally? | tally at the end of the roundtally หลังจบรอบ |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 |
Show the fixed code ดูโค้ดที่แก้แล้ว
The bug is the indentation of the line tally[vote] += 1. It sits inside if vote not in tally:, so it runs only the first time each candidate appears, and every candidate ends with 1 vote. In the flowchart the box "Add one vote" is on the Yes path only, but the problem needs one vote for every ballot. Move the line 4 spaces to the left so that it lines up with if; then it runs once for every ballot. It is a logic error: the indentation is still valid Python, so the program runs to the end with no error message. D and ABCDE hide the bug, because there every candidate really has 1 vote. And because every count is 1, find_winner always picks the first ballot: that is why BACBB still names the right winner but shows the wrong number of votes.
bug อยู่ที่การย่อหน้าของบรรทัด tally[vote] += 1 บรรทัดนี้อยู่ใน if vote not in tally: จึงทำงานเฉพาะครั้งแรกที่ผู้สมัครแต่ละคนปรากฏ ผู้สมัครทุกคนจึงได้ 1 คะแนนเสมอ ใน flowchart กล่อง "Add one vote" อยู่บนทาง Yes เท่านั้น แต่โจทย์ต้องการเพิ่ม 1 คะแนนให้บัตรโหวตทุกใบ แก้โดยเลื่อนบรรทัดนี้ไปทางซ้าย 4 ช่องให้ตรงกับ if บรรทัดนี้จึงทำงานทุกครั้งที่อ่านบัตรโหวต 1 ใบ เป็น logic error เพราะการย่อหน้าแบบนี้ยังเป็น Python ที่ถูกต้อง โปรแกรมจึงรันจบโดยไม่มีข้อความ error ส่วน D และ ABCDE ซ่อน bug ไว้ เพราะผู้สมัครทุกคนได้ 1 คะแนนจริงๆ และเมื่อทุกคนได้ 1 คะแนน find_winner จึงเลือกบัตรโหวตใบแรกเสมอ BACBB จึงยังบอกผู้ชนะถูก แต่จำนวนคะแนนโหวตผิด
| Roundรอบ | vote | vote not in tally? | tally at the end of the roundtally หลังจบรอบ |
|---|---|---|---|
| 1 | B | ✓ | {'B': 1} |
| 2 | A | ✓ | {'B': 1, 'A': 1} |
| 3 | B | ✗ | {'B': 1, 'A': 1} |
After the fix, round 3 gives {'B': 2, 'A': 1}.
หลังแก้แล้ว รอบที่ 3 จะได้ {'B': 2, 'A': 1}
Enter the ballots: BACBB Winner: B Votes: 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 — Tutorial: Dictionaries
(keys,
in/not in, and looping over a dictionary in the order the keys were added) (key,in/not inและการวน loop บน dictionary ตามลำดับที่เพิ่ม key) - Python docs — Lexical analysis: Indentation (how indentation decides which block a line belongs to) (การย่อหน้าบอกว่าบรรทัดนั้นอยู่ใน block ไหน)
- Kinds of error, kinds of test case and bug-hunting tools: 11 Testing and Debugging; dictionaries: 08 Dictionary; flowchart symbols: 01 Basic → Flowchart Basic. ชนิดของ error, ชนิดของ test case และเครื่องมือหา bug: 11 การทดสอบและการแก้ไขโปรแกรม ; dictionary: 08 Dictionary ; สัญลักษณ์ของ flowchart: 01 Basic → Flowchart Basic