Debugging א': מהו Bug, סוגי שגיאות וקריאת Stack Trace

מטרות השיעור
  • להגדיר מהו Bug ולהבחין בין Syntax Error, Runtime Error ו-Logical Error
  • לקרוא Stack Trace — לזהות סוג שגיאה, הודעה, קובץ ושורה
  • להכיר את השגיאות הנפוצות ב-Python ואת הסיבה לכל אחת
  • להבין למה Logical Error היא הקשה ביותר למציאה

מה נלמד?

  • מהו Bug.
  • שלושת סוגי השגיאות: Syntax Error, Runtime Error, Logical Error.
  • איך קוראים Stack Trace (Traceback), הודעת השגיאה של Python.
  • השגיאות הנפוצות ביותר, ומה כל אחת אומרת.

למה צריך את זה?

מפתחים מבלים חלק ענק מהזמן במציאת באגים ותיקונם. ההבדל בין מתחיל למקצוען הוא לא שלמקצוען אין באגים, אלא שהוא קורא את הודעת השגיאה ויודע תוך שניות איפה לחפש. מתחילים רואים הרבה טקסט אדום ונבהלים. בסוף השיעור, הטקסט האדום יהפוך למפה.

הסבר מהבסיס

מהו Bug?

Bug (באג) הוא פער בין מה שהתוכנה אמורה לעשות לבין מה שהיא עושה בפועל. לפעמים התוכנה קורסת, ולפעמים היא “עובדת” אבל מחזירה תוצאה שגויה.

למה קוראים לזה Bug?

ב-1947 מצאו מהנדסים של מחשב Mark II עש (bug) תקוע בתוך אחד הממסרים, והדביקו אותו ביומן העבודה עם הכיתוב “first actual case of bug being found”. המילה הייתה בשימוש עוד קודם לתקלות טכניות, אבל הסיפור הזה הפך אותה למפורסמת.

שלושת סוגי השגיאות

סוגמתי מתגלההאם Python מודיעה?דוגמה
Syntax Errorלפני הריצה (בבדיקת התחביר)כן, ושום שורה לא רצהחסר : או סוגר
Runtime Error (Exception)בזמן הריצה, בשורה הבעייתיתכן, והשורות שלפניה כבר רצוחלוקה ב-0, משתנה לא קיים
Logical Errorאף פעם אוטומטיתלא! הקוד רץ “בהצלחה”+ במקום -, תנאי הפוך

זוכרים את נושא מחשב, מערכת הפעלה ורשת? Syntax Error נתפסת כש-Python מתרגמת ל-Bytecode, לפני הביצוע. Runtime Error קורית כשה-PVM מבצע שורה ספציפית ונכשל.

Logical Error היא המסוכנת ביותר: אין הודעה, אין צבע אדום, רק תוצאה שגויה. מוצאים אותה רק אם בודקים שהתוצאה נכונה.

Stack Trace — מפת השגיאה

כש-Runtime Error קורית, Python מדפיסה Traceback (נקרא גם Stack Trace): המסלול שהקוד עשה עד לשגיאה.

Traceback (most recent call last):
  File "shop.py", line 9, in <module>
    total = checkout(cart)
  File "shop.py", line 5, in checkout
    return subtotal / count
ZeroDivisionError: division by zero

קוראים מלמטה למעלה:

  1. השורה האחרונה — סוג השגיאה (ZeroDivisionError) והודעה (division by zero). מתחילים תמיד מכאן.
  2. השורה שמעליה — איפה בדיוק: קובץ shop.py, שורה 5, בתוך הפונקציה checkout, והקוד עצמו.
  3. מעליה — מי קרא לפונקציה הזו: שורה 9, ברמה הראשית של הקובץ (<module>).

למה “Stack”? כי כל קריאה לפונקציה נערמת על המחסנית (Call Stack) של הקריאות. ה-Traceback מציג את הערימה ברגע הקריסה, מהקריאה הראשונה (למעלה) ועד האחרונה (למטה).

השגיאות הנפוצות ב-Python

שגיאהמשמעותסיבה נפוצה
SyntaxErrorהקוד לא תקין תחביריתחסר :, סוגריים, מירכאות
IndentationErrorהזחה שגויהערבוב רווחים, הזחה לא עקבית
NameErrorשם לא מוגדרשגיאת כתיב, שימוש לפני הגדרה
TypeErrorפעולה על טיפוס לא מתאים"5" + 3, קריאה לפונקציה עם מספר ארגומנטים לא נכון
ValueErrorטיפוס נכון, ערך לא תקיןint("hello")
ZeroDivisionErrorחלוקה באפסמכנה שלא נבדק
IndexErrorאינדקס מחוץ לרשימהitems[5] כשיש 3 איברים
KeyErrorמפתח לא קיים ב-dict(נלמד בנושא מבני נתונים ופונקציות)
AttributeErrorאין לאובייקט תכונה/מתודה כזו"abc".push(), שגיאת כתיב במתודה
FileNotFoundErrorקובץ לא קייםנתיב שגוי, Working Directory

תחביר

אין תחביר חדש. הכלי הוא קריאה. התבנית של כל Traceback:

Traceback (most recent call last):
  File "<file>", line <N>, in <function or <module>>
    <the code on that line>
  ...                                  ← more frames, each a function call
<ErrorType>: <message>                 ← START READING HERE

ושל Syntax Error (בלי Traceback, כי שום דבר לא רץ):

  File "<file>", line <N>
    <the code>
        ^                              ← where Python noticed the problem
SyntaxError: <message>

דוגמת קוד ראשונה

week-2/bug_report.py:

def average(numbers):
    total = sum(numbers)
    return total / len(numbers)


scores = []
print("Average score:", average(scores))

פלט:

Traceback (most recent call last):
  File "C:\dev\kodkod6\week-2\bug_report.py", line 7, in <module>
    print("Average score:", average(scores))
                            ^^^^^^^^^^^^^^^
  File "C:\dev\kodkod6\week-2\bug_report.py", line 3, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

פירוק הקוד שורה אחר שורה

קריאת ה-Traceback מלמטה למעלה:

שורה בפלטמה היא אומרת לנו
ZeroDivisionError: division by zeroמה קרה: חלוקה באפס.
line 3, in average + return total / len(numbers)איפה: שורה 3, בתוך average. ה-~~~^~~~ מסמן בדיוק את פעולת החילוק.
line 7, in <module> + average(scores)מי קרא: שורה 7, ברמה הראשית. <module> = “לא בתוך פונקציה”.

הסקת המסקנה: len(numbers) הוא 0, כלומר הרשימה ריקה. ומי העביר רשימה ריקה? שורה 7 העבירה את scores, שהוא []. הבאג האמיתי הוא שלא טיפלנו במקרה של רשימה ריקה. השורה שקרסה (3) היא הסימפטום, וההחלטה מה לעשות ברשימה ריקה היא התיקון:

def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)

דוגמה נוספת

שלושת סוגי השגיאות על אותה פונקציה. הפונקציה אמורה לחשב מחיר אחרי הנחה באחוזים.

Syntax Error:

def price_after_discount(price, percent)
    return price - price * percent / 100
  File "discount.py", line 1
    def price_after_discount(price, percent)
                                            ^
SyntaxError: expected ':'

חסרות נקודתיים. Python לא הריצה אף שורה.

Runtime Error:

def price_after_discount(price, percent):
    return price - price * percent / 100

print(price_after_discount("100", 10))
Traceback (most recent call last):
  File "discount.py", line 4, in <module>
    print(price_after_discount("100", 10))
  File "discount.py", line 2, in price_after_discount
    return price - price * percent / 100
                   ~~~~~~~~~~~~~~~~^~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'int'

התחביר תקין, אבל בזמן ריצה price הוא טקסט. "100" * 10 “עובד” (חוזר על הטקסט 10 פעמים!), אבל אז החילוק ב-100 נכשל. ה-^ מצביע על ה-/.

Logical Error:

def price_after_discount(price, percent):
    return price - percent / 100

print(price_after_discount(100, 10))
99.9

אין שום שגיאה! אבל הנחה של 10% על 100 צריכה להחזיר 90.0. שכחנו להכפיל ב-price. Python לא יכולה לדעת מה התכוונו. רק בדיקה מול תוצאה צפויה תגלה את זה.

טעות נפוצה

הטעות 1: לקרוא את ה-Traceback מלמעלה, ולהיבהל. תמיד מתחילים מהשורה האחרונה: סוג ההודעה. אחר כך השורה שמעליה. רוב הזמן אלה כל מה שצריך.

הטעות 2: לתקן את השורה שקרסה, במקום את הסיבה. בדוגמה הראשונה, “פתרון” כמו return total / (len(numbers) + 1) מעלים את השגיאה, אבל מחזיר תוצאה שגויה בכל מקרה! זה הפך Runtime Error ל-Logical Error. יותר גרוע.

הטעות 3: לחפש את ה-Syntax Error בשורה שמופיעה בהודעה בלבד. לפעמים הבעיה בשורה הקודמת:

prices = [10, 20, 30
total = sum(prices)
  File "test.py", line 1
    prices = [10, 20, 30
             ^
SyntaxError: '[' was never closed

Python הגיעה לשורה 2, וגילתה שהסוגר של שורה 1 לא נסגר. בגרסאות חדשות ההודעה מדויקת, אבל באופן כללי: אם השורה המסומנת נראית תקינה, בדקו את זו שלפניה.

תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

תרגיל 1: איזה סוג שגיאה?

לכל קטע — האם זו SyntaxError, Runtime Error או Logical Error? ומה הבעיה?

# A
if age > 18
    print("adult")

# B
print(total_price)        # total_price was never defined

# C
def area(width, height):
    return width + height   # should be the area of a rectangle
הצג פתרון
  • A — SyntaxError: חסרות נקודתיים : בסוף שורת ה-if. שום שורה לא רצה.
  • B — Runtime Error (NameError): התחביר תקין, אבל בזמן ריצה Python לא מוצאת משתנה בשם total_price.
  • C — Logical Error: אין שום שגיאה, אבל התוצאה לא נכונה — שטח מחשבים בכפל, width * height. את השגיאה הזו מוצאים רק אם בודקים את התוצאה.

תרגיל 2: קריאת Stack Trace

Traceback (most recent call last):
  File "shop.py", line 12, in <module>
    print(checkout([20, 30], 0))
  File "shop.py", line 7, in checkout
    return average_price(prices, count)
  File "shop.py", line 3, in average_price
    return sum(prices) / count
ZeroDivisionError: division by zero
  1. מה סוג השגיאה ומה ההודעה?
  2. באיזה קובץ, באיזו שורה ובאיזו פונקציה היא קרתה?
  3. מאיפה הגיע הערך הבעייתי?
הצג פתרון
  1. ZeroDivisionError: division by zero — חלוקה באפס. קוראים את השורה האחרונה קודם.
  2. בקובץ shop.py, שורה 3, בפונקציה average_price — זו השורה הכי נמוכה ברשימה.
  3. קוראים למעלה: average_price נקראה משורה 7 ב-checkout, ו-checkout נקראה משורה 12 עם count = 0. כלומר הבעיה התחילה בקריאה בשורה 12, שהעבירה 0.

תרגיל 3: באג לוגי בהמרת טמפרטורה

def to_celsius(fahrenheit):
    return fahrenheit - 32 * 5 / 9

print(to_celsius(212))
print(to_celsius(32))

התוצאות צריכות להיות 100 ו-0, אבל יוצא משהו אחר. מצאו את הבאג ותקנו.

פלט צפוי:

100.0
0.0
הצג פתרון
def to_celsius(fahrenheit):
    return (fahrenheit - 32) * 5 / 9


print(to_celsius(212))
print(to_celsius(32))
100.0
0.0

סדר פעולות: כפל וחילוק קודמים לחיסור, ולכן הקוד המקורי חישב fahrenheit - (32 * 5 / 9). הסוגריים מכריחים את החיסור לקרות קודם. זו Logical Error — אין הודעת שגיאה, רק תוצאה לא נכונה.

תרגיל 4: TypeError

age = 25
print("Age: " + age)
  1. איזו שגיאה תתקבל, ולמה?
  2. תקנו בשתי דרכים שונות.

פלט צפוי:

Age: 25
הצג פתרון
  1. TypeError: can only concatenate str (not "int") to str — אי אפשר לחבר טקסט ומספר עם +.
  2. שתי דרכים:
age = 25
print("Age: " + str(age))
Age: 25
age = 25
print("Age:", age)
Age: 25

הראשונה ממירה את המספר לטקסט. השנייה מעבירה ל-print שני ערכים, והוא מוסיף רווח ביניהם.

תרגיל 5: ממוצע של רשימה ריקה

def average(numbers):
    return sum(numbers) / len(numbers)

print(average([80, 90]))
print(average([]))
  1. מה יקרה בקריאה השנייה, ואיזו שגיאה תתקבל?
  2. תקנו את הפונקציה כך שעבור רשימה ריקה היא תחזיר 0.

פלט צפוי:

85.0
0
הצג פתרון
  1. len([]) הוא 0, ולכן מתקבל ZeroDivisionError: division by zero — שגיאת זמן ריצה.
  2. בודקים את המקרה הזה לפני החלוקה:
def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)


print(average([80, 90]))
print(average([]))
85.0
0

סיכום

  • Bug = פער בין ההתנהגות הרצויה לבין ההתנהגות בפועל.
  • Syntax Error — לפני הריצה, שום שורה לא רצה. Runtime Error — באמצע הריצה, עם Traceback. Logical Error — אין הודעה, רק תוצאה שגויה.
  • Traceback קוראים מלמטה למעלה: סוג + הודעה ‏←‏ שורה ופונקציה ‏←‏ מי קרא.
  • השורה שקרסה היא לפעמים רק הסימפטום. מחפשים את הסיבה.
  • Logical Errors מתגלים רק כשמשווים תוצאה לערך צפוי.

דוקומנטציה רשמית