Exceptions ב': else ו-finally — המבנה המלא

מטרות השיעור
  • להסביר מתי רץ כל אחד מארבעת הבלוקים — try, except, else, finally
  • להשתמש ב-else לקוד שרץ רק אם לא הייתה שגיאה
  • להשתמש ב-finally לפעולות ניקוי שחייבות לקרות תמיד
  • לחזות את סדר ההדפסות בתוכנית עם try מלא

מה נלמד?

בשיעור הקודם השתמשנו ב-try וב-except. למבנה יש עוד שני חלקים:

  • else — רץ רק אם לא הייתה שגיאה.
  • finally — רץ תמיד, עם שגיאה או בלעדיה.

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

דמיינו קוד שפותח חיבור ל-Database (נושא Backend ומסדי נתונים), מבצע פעולה וסוגר את החיבור. מה קורה אם הפעולה נכשלת באמצע? אם הסגירה כתובה אחרי הפעולה, היא לא תתבצע, והחיבור יישאר פתוח. אחרי מאה כישלונות כאלה, השרת יקרוס כי נגמרו לו החיבורים.

finally מבטיח שהניקוי יקרה בכל מקרה. else שומר על ה-try קטן, ומבהיר מה רץ רק אחרי הצלחה.

הסבר מהבסיס

ארבעת הבלוקים

בלוקמתי רץלמה משמש
tryתמיד מתחילים בוהקוד שעלול להיכשל
exceptרק אם נזרקה שגיאה מהסוג המתאיםטיפול בשגיאה
elseרק אם לא נזרקה שום שגיאה ב-tryהמשך שתלוי בהצלחה
finallyתמיד, בסוף, בכל מקרהניקוי: סגירת קבצים, חיבורים, הודעת סיום
אנלוגיה: בישול

try = מנסים לאפות עוגה. except = אם נשרפה, מזמינים עוגה מהמאפייה. else = אם יצאה טובה, מקשטים אותה (לא מקשטים עוגה שרופה!). finally = בכל מקרה, מכבים את התנור ושוטפים כלים.

למה else ולא פשוט להמשיך ב-try?

אפשר לשים את קוד ההמשך בתוך ה-try. אבל אז, אם הקוד הזה נכשל באותה שגיאה, ה-except יתפוס אותו בטעות, ויטפל בבעיה הלא נכונה. else מבהיר: רק השורה המסוכנת בתוך try, והשאר בחוץ.

finally רץ גם כש…

  • הייתה שגיאה ונתפסה.
  • הייתה שגיאה ולא נתפסה. finally רץ, ואז התוכנית קורסת.
  • יש return באמצע ה-try. finally רץ לפני שהפונקציה באמת חוזרת.

תחביר

try:
    # risky code
except SomeError:
    # runs only if SomeError happened
else:
    # runs only if NO error happened in try
finally:
    # runs ALWAYS, last

סדר חובה: try ‏←‏ except (אחד או יותר) ‏←‏ else (אופציונלי) ‏←‏ finally (אופציונלי). אפשר גם try + finally בלי except.

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

week-2/full_try.py:

def convert(text):
    print("--- converting:", text)
    try:
        number = int(text)
    except ValueError:
        print("except: not a number")
    else:
        print("else: success, doubled =", number * 2)
    finally:
        print("finally: done with", text)


convert("21")
convert("hi")

פלט:

--- converting: 21
else: success, doubled = 42
finally: done with 21
--- converting: hi
except: not a number
finally: done with hi

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

בלוקconvert("21")convert("hi")
try: number = int(text)מצליח: number = 21ValueError
except ValueError:מדולגרץ
else:רץ: מדפיס 42מדולג
finally:רץרץ

else ו-except לעולם לא רצים שניהם: או שהייתה שגיאה, או שלא. ואילו finally רץ בשני המקרים.

דוגמה נוספת

“חיבור” שחייב להיסגר. כאן מדמים חיבור למשאב חיצוני (כמו Database), ורואים ש-finally סוגר גם כשיש כישלון באמצע, וגם כשיש return. week-2/connection_demo.py:

def open_connection():
    print("[connection opened]")


def close_connection():
    print("[connection closed]")


def fetch_price(product_name, prices):
    open_connection()
    try:
        price = prices[product_name]
    except KeyError:
        print("No such product:", product_name)
        return None
    else:
        print("Found", product_name)
        return price
    finally:
        close_connection()


catalog = {"bread": 8, "milk": 6}

print("Result:", fetch_price("milk", catalog))
print()
print("Result:", fetch_price("cake", catalog))

פלט:

[connection opened]
Found milk
[connection closed]
Result: 6

[connection opened]
No such product: cake
[connection closed]
Result: None

פירוק:

  • catalog = {"bread": 8, "milk": 6} — זה dict (מילון): כל מפתח ("bread") ממופה לערך (8). prices["milk"] מחזיר 6. נלמד לעומק בנושא מבני נתונים ופונקציות. כרגע מספיק לדעת שגישה למפתח שלא קיים זורקת KeyError.
  • שימו לב: יש return גם ב-except וגם ב-else. ובכל זאת [connection closed] מודפס לפני ה-Result. כלומר, finally רץ אחרי ה-return “הוחלט”, אבל לפני שהפונקציה באמת חוזרת.
  • בלי finally, היינו צריכים לכתוב close_connection() לפני כל return. קל לשכוח, והרבה כפילות.

טעות נפוצה

הטעות 1: לשים ניקוי אחרי ה-try/except במקום ב-finally.

open_connection()
try:
    price = prices[name]
except KeyError:
    print("missing")
    return None
close_connection()   # never reached when except returns!

במקרה של KeyError, ה-return יוצא מהפונקציה, ו-close_connection() לא רץ. החיבור נשאר פתוח. finally פותר בדיוק את זה.

הטעות 2: קוד ההמשך בתוך ה-try, ו-except שתופס את השגיאה הלא נכונה.

try:
    age = int(text)
    category = ["child", "adult"][age // 18]   # can also raise IndexError... or ValueError?
except ValueError:
    print("Age must be a number")

אם שורת ההמשך תיכשל, למשל ב-IndexError עבור גיל 40 (40 // 18 = 2, ואין אינדקס 2), ההודעה לא קשורה בכלל. שימו בתוך ה-try רק את int(text), ואת ההמשך ב-else.

הטעות 3: לחשוב ש-finally “תופס” את השגיאה. הוא לא. אם אין except מתאים, finally רץ, ואחר כך התוכנית קורסת עם Traceback.

תרגול

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

תרגיל 1: חזו את הפלט

כתבו מה יודפס עבור כל קריאה — לפני שמריצים:

def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)

half("8")
half("x")
הצג פתרון
def half(text):
    try:
        number = int(text)
    except ValueError:
        print("not a number:", text)
    else:
        print("half is", number / 2)
    finally:
        print("checked", text)


half("8")
half("x")
half is 4.0
checked 8
not a number: x
checked x
  • עבור "8" אין שגיאה, ולכן רץ else, ואחריו finally.
  • עבור "x" נזרקת ValueError, ולכן רץ except — ו-else לא רץ. finally רץ בשני המקרים.

תרגיל 2: finally רץ גם אחרי return

def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)

print(check(5))
print(check(-2))

מה יודפס, ובאיזה סדר? הסבירו למה.

הצג פתרון
def check(number):
    try:
        if number > 0:
            return "positive"
        return "not positive"
    finally:
        print("done checking", number)


print(check(5))
print(check(-2))
done checking 5
positive
done checking -2
not positive

finally רץ לפני שהפונקציה באמת חוזרת — גם כשיש return בתוך ה-try. לכן “done checking” מודפס לפני הערך שהפונקציה החזירה.

תרגיל 3: ארבעת הבלוקים

כתבו פונקציה parse_quantity(text) שמשתמשת בכל ארבעת הבלוקים:

  • try — ממיר את הטקסט למספר;
  • except — מדפיס invalid: <text>;
  • else — מדפיס quantity: <number>;
  • finally — מדפיס ---.
parse_quantity("3")
parse_quantity("three")

פלט צפוי:

quantity: 3
---
invalid: three
---
הצג פתרון
def parse_quantity(text):
    try:
        quantity = int(text)
    except ValueError:
        print("invalid:", text)
    else:
        print("quantity:", quantity)
    finally:
        print("---")


parse_quantity("3")
parse_quantity("three")
quantity: 3
---
invalid: three
---

הקוד שרץ רק כשהכול הצליח נמצא ב-else ולא בתוך ה-try — כך ה-try נשאר קטן, ותופס רק את השורה שבאמת עלולה להיכשל.

תרגיל 4: לסגור את מה שפתחנו

הפונקציות open_register() ו-close_register() פותחות וסוגרות קופה. כתבו פונקציה sell(prices, position) שפותחת את הקופה, מחזירה את המחיר במיקום שנבחר (המיקום הוא אינדקס של הרשימה — מתחיל מ-0), ותמיד סוגרת את הקופה — גם כשהמיקום לא קיים. מיקום לא קיים מחזיר None.

def open_register():
    print("register opened")

def close_register():
    print("register closed")

print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))

פלט צפוי:

register opened
register closed
20
register opened
register closed
None
הצג פתרון
def open_register():
    print("register opened")


def close_register():
    print("register closed")


def sell(prices, position):
    open_register()
    try:
        return prices[position]
    except IndexError:
        return None
    finally:
        close_register()


print(sell([10, 20, 30], 1))
print(sell([10, 20, 30], 7))
register opened
register closed
20
register opened
register closed
None

זה בדיוק התפקיד של finally: ניקוי — סגירת משאב שנפתח, לא משנה איך הפונקציה הסתיימה.

תרגיל 5: else או בתוך ה-try?

שני מפתחים כתבו אותו קוד בשתי דרכים:

# A                              # B
try:                             try:
    number = int(text)               number = int(text)
    print(100 / number)          except ValueError:
except ValueError:                   print("not a number")
    print("not a number")        else:
                                     print(100 / number)

מה ההבדל, ואיזו גרסה עדיפה? מה יקרה בכל אחת כש-text הוא "0"?

הצג פתרון
  • ב-A, גם החלוקה נמצאת בתוך ה-try. ב-B, בתוך ה-try יש רק את ההמרה, והחלוקה עברה ל-else.
  • כש-text הוא "0", בשתי הגרסאות תיזרק ZeroDivisionError, שאף except לא תופס, והתוכנית תקרוס. אבל ב-B ברור לגמרי שה-except ValueError שייך רק להמרה.
  • B עדיפה: try קטן ככל האפשר. כך except לא תופס בטעות שגיאות שמגיעות משורות אחרות, וקל לראות מה כל חלק עושה.

סיכום

  • try — קוד מסוכן. except — רק אם הייתה שגיאה מתאימה. else — רק אם לא הייתה שגיאה. finally — תמיד.
  • except ו-else לעולם לא רצים יחד.
  • finally רץ גם אחרי return, וגם כשהשגיאה לא נתפסה (ואז התוכנית קורסת אחריו).
  • else שומר את ה-try קטן וממוקד. finally הוא המקום לניקוי: סגירת קבצים וחיבורים.

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