Exceptions א': למה צריך טיפול בשגיאות, try ו-except

מטרות השיעור
  • להסביר מהו Exception ומה קורה לתוכנית כשהוא לא מטופל
  • לכתוב בלוק try / except שתופס שגיאה ומטפל בה
  • לטפל בכמה סוגי Exceptions בנפרד, ולקבל את פרטי השגיאה עם as
  • להבין למה except “ריק” שתופס הכול הוא הרגל מסוכן

מה נלמד?

  • מהו Exception (חריגה).
  • למה תוכנה אמיתית צריכה לטפל בשגיאות ולא לקרוס.
  • התחביר try / except.
  • טיפול שונה בסוגי שגיאות שונים.

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

דמיינו שרת מחשבון קטן: לקוח אחד שולח "hello" במקום מספר, והשרת כולו קורס. כל הלקוחות שבאים אחריו מקבלים “Connection refused”.

בעולם האמיתי:

  • משתמש מקליד “abc” בשדה גיל. האפליקציה לא אמורה להיסגר.
  • קובץ שהתוכנית מנסה לקרוא נמחק. צריך להודיע, לא לקרוס.
  • האינטרנט נפל באמצע בקשה. כדאי לנסות שוב, או להציג הודעה.

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

הסבר מהבסיס

מהו Exception?

Exception הוא אובייקט ש-Python “זורקת” (Raise) כש-Runtime Error קורה. כל השגיאות שראינו אתמול (ValueError, ZeroDivisionError, TypeError…) הן Exceptions.

כש-Exception נזרק:

  1. Python עוצרת את השורה הנוכחית מיד.
  2. היא מחפשת מי “יתפוס” (Catch) את השגיאה.
  3. אם אף אחד לא תופס, התוכנית קורסת ומודפס Traceback.

ה-try / except הוא הדרך לתפוס.

אנלוגיה: רשת ביטחון בקרקס

לוליין שהולך על חבל (הקוד שלכם) עלול ליפול (Exception). בלי רשת, הנפילה היא סוף ההופעה (קריסה). try = “ההופעה מעל רשת”. except = הרשת, שתופסת את הנפילה, ואז מחליטים מה לעשות: לנסות שוב, להודיע לקהל, או להמשיך לקטע הבא.

איך זה עובד

  • try: — “נסה לבצע את הבלוק הזה”.
  • except SomeError: — “אם בתוך ה-try נזרק SomeError, קפוץ לכאן ובצע את הבלוק הזה במקום לקרוס”.
  • אם לא נזרקה שגיאה, בלוק ה-except מדולג.
  • אחרי ה-try/except, התוכנית ממשיכה כרגיל.

תחביר

try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...

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

week-2/safe_age.py:

text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")

הרצה 1 (קלט תקין):

Enter your age: 30
Next year you will be 31
Program continues normally.

הרצה 2 (קלט שגוי):

Enter your age: abc
That is not a whole number: abc
Program continues normally.

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

שורההרצה 1 (30)הרצה 2 (abc)
text = input(...)text = "30"text = "abc"
try:נכנסים לבלוקנכנסים לבלוק
age = int(text)מצליח: age = 30נזרק ValueError, ו-Python קופצת מיד ל-except
print("Next year...")מודפסמדולג. לא מגיעים אליה
except ValueError:מדולג (אין שגיאה)נתפס!
print("That is not...")לא רץמודפס
print("Program continues...")מודפסמודפס, והתוכנית לא קרסה

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

דוגמה נוספת

מחשבון חלוקה עמיד, עם טיפול שונה לכל סוג שגיאה ולולאה שמבקשת שוב. week-2/safe_divide.py:

def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")


def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0


x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))

הרצה לדוגמה:

First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0

פירוק:

  • return float(text) בתוך try — אם ההמרה מצליחה, הפונקציה מחזירה מיד ויוצאת גם מהלולאה. אם נכשלת, except מדפיס הודעה, והלולאה מבקשת שוב. כך “מחייבים” את המשתמש לקלט תקין.
  • כל פונקציה תופסת את השגיאה שרלוונטית אליה: ask_number את ValueError (קלט), divide את ZeroDivisionError (חישוב).
  • שאלה פתוחה: האם divide צריכה להחזיר 0 בחלוקה באפס? מתמטית, זו תשובה שגויה! במקרים רבים עדיף לא לתפוס כאן, ולתת למי שקרא להחליט. נדבר על זה בשיעור על raise.

as error — לקבל את פרטי השגיאה

try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
Conversion failed: invalid literal for int() with base 10: '12.5'

error הוא אובייקט השגיאה. הדפסה שלו מציגה את ההודעה שהייתה מופיעה בשורה האחרונה של ה-Traceback.

טעות נפוצה

הטעות 1: except ריק שתופס הכול.

try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")

יש כאן שלוש בעיות שונות (קלט, חלוקה באפס, ואפילו אות שחסרה בשם המשתנה), וכולן מוסתרות תחת “Something went wrong”. אי אפשר לדבג. והגרוע מכל: except: ריק תופס גם KeyboardInterrupt, ולכן אפילו Ctrl + C לא יעצור תוכנית שתקועה בלולאה כזו!

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

הטעות 2: try ענק.

try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")

איזו מ-40 השורות נכשלה? לא ידוע. שמים ב-try רק את השורות שעלולות להיכשל בשגיאה שתופסים, כמה שפחות.

הטעות 3: “לבלוע” שגיאה בשקט.

try:
    price = float(text)
except ValueError:
    pass

pass אומר “אל תעשה כלום”. השגיאה נעלמת, ו-price אולי לא קיים בכלל, מה שיגרום ל-NameError מבלבל כמה שורות אחר כך. תמיד עושים משהו: הודעה, ערך ברירת מחדל מוסבר, או רישום ללוג.

תרגול

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

תרגיל 1: המרה בטוחה למספר

כתבו פונקציה to_number(text) שמחזירה את הטקסט כמספר שלם, ואם הטקסט אינו מספר — מחזירה None, בלי שהתוכנית תקרוס.

print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))

פלט צפוי:

42
None
7
הצג פתרון
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
42
None
7

int("hello") זורקת ValueError. בלי try, התוכנית הייתה קורסת. עם except ValueError אנחנו מחליטים מה קורה במקום.

תרגיל 2: טיפול שונה לכל סוג שגיאה

כתבו פונקציה safe_divide(a_text, b_text) שמקבלת שני טקסטים, ממירה אותם למספרים ומחלקת. היא מחזירה:

  • את תוצאת החלוקה, אם הכול תקין;
  • "numbers only" אם אחד הטקסטים אינו מספר;
  • "cannot divide by zero" אם המחלק הוא 0.
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))

פלט צפוי:

2.5
cannot divide by zero
numbers only
הצג פתרון
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
2.5
cannot divide by zero
numbers only

לכל סוג שגיאה יש except משלו, עם טיפול מתאים. Python בודקת את בלוקי ה-except לפי הסדר ונכנסת לראשון שמתאים.

תרגיל 3: להציג את הודעת השגיאה

הקוד הבא נכשל עם ValueError. עטפו אותו ב-try, תפסו את השגיאה עם except ValueError as error, והדפיסו את הטקסט Could not convert: ואחריו את ההודעה ש-Python שמרה ב-error:

number = int("12abc")

פלט צפוי:

Could not convert: invalid literal for int() with base 10: '12abc'
הצג פתרון
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
Could not convert: invalid literal for int() with base 10: '12abc'

as error שומר את אובייקט השגיאה במשתנה. כשמדפיסים אותו, מקבלים את ההודעה ש-Python יצרה.

תרגיל 4: בחירה מרשימה

נתונה רשימת שמות. כתבו פונקציה pick(names, position_text) שמקבלת מיקום כטקסט (מתחיל מ-1) ומחזירה את השם במיקום הזה. היא מחזירה "not a number" לקלט שאינו מספר, ו-"no such position" למיקום שלא קיים.

names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))

פלט צפוי:

Avi
not a number
no such position
הצג פתרון
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
Avi
not a number
no such position

שתי שגיאות שונות בשני מקומות: int(...) עלולה לזרוק ValueError, וגישה לרשימה עלולה לזרוק IndexError.

תרגיל 5: except שתופס הכול

def read_age(text):
    try:
        return int(text)
    except:
        return 0
  1. למה except: בלי סוג שגיאה מסוכן?
  2. תנו דוגמה לבאג אמיתי שהקוד הזה היה מסתיר.
  3. תקנו את הקוד.
הצג פתרון
  1. except: ריק תופס כל שגיאה — גם כאלה שלא חשבנו עליהן — ומסתיר אותן. הקוד ממשיך כאילו הכול בסדר, עם ערך שגוי.
  2. אם מישהו יעביר בטעות None במקום טקסט, תיזרק TypeError — באג אמיתי בקוד שקרא לפונקציה. במקום שנראה אותו, נקבל בשקט גיל 0. (אפילו Ctrl + C נתפס על ידי except: ריק.)
  3. תופסים רק את השגיאה שאנחנו מצפים לה:
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
30
0

סיכום

  • Exception = שגיאה ש-Python זורקת בזמן ריצה. אם אף אחד לא תופס אותה, התוכנית קורסת.
  • try = “נסה”. except SomeError = “אם קרתה השגיאה הזו, עשה את זה במקום לקרוס”.
  • ברגע שנזרקת שגיאה, שאר ה-try מדולג.
  • except SomeError as error נותן גישה להודעת השגיאה.
  • תופסים שגיאות ספציפיות בלבד, עם try קטן. בלי except: ריק ובלי pass שבולע שגיאות.

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