• בלוג
  • טיפול ב Exceptions ב Python

טיפול ב Exceptions ב Python

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

1. למה צריך Exceptions

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

print(sum_of_digits(941))

יכול להיות שגם נרצה להרחיב את התוכנית ולתת למשתמש לבחור מספר ואז להדפיס את סכום הספרות באותו מספר:

num = input("Please select a number: ")
print(sum_of_digits(num))

הבעיה שלפונקציה sum_of_digits יש גם ציפיות משלה מהעולם - למשל היא מצפה שיפעילו אותה עם מספר. מה קורה אם המשתמש העביר את הטקסט fourty two? יכול להיות שהמשתמש התכוון לקבל את התוצאה 6, אבל הפונקציה sum_of_digits לא ממש יודעת מזה וגם לא יודעת איך לטפל בטקסטים.

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

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

מנגנון ה Exceptions הוא המנגנון בו פונקציות מדווחות על בעיה. הוא עדיף על החזרת ערך "שגיאה" כי לא תמיד מי שכותב את הקוד שקורא לפונקציה זוכר לבדוק את ערכי השגיאה ולכן מנגנון Exceptions עמיד יותר בפני טעויות. בנוסף מבנה של Exceptions מאפשר "דילוגים" - כלומר פונקציה A קוראת לפונקציה B שקוראת לפונקציה C וזו האחרונה נתקלה בבעיה. באמצעות Exceptions הפונקציה C מדווחת על בעיה, ואם B לא עושה כלום אז באופן אוטומטי הבעיה עוברת לטיפול של A שקראה לה.

2. איך זה עובד

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

נראה דוגמא בשפת Python. הפונקציה int לוקחת מחרוזת שכוללת מספר והופכת אותה לטיפוס מספרי. כך נראית תוכנית שמקבלת שני מספרים מהמשתמש ומדפיסה את סכומם:

x = int(input("Please select a first number: "))
y = int(input("Please select a second number: "))

print(f"{x} + {y} = {x + y}")

עכשיו ננסה להפעיל אותה ולהעביר משהו שאינו מספר בתור אחד הפרמטרים:

Please select a first number: one
Traceback (most recent call last):
  File "demo1.py", line 1, in <module>
    x = int(input("Please select a first number: "))
ValueError: invalid literal for int() with base 10: 'one'

הפונקציה int הודיעה לנו שהיא לא יכולה להפוך את המחרוזת one למספר - וזה הגיוני. מאחר ואין לנו קוד שמטפל באירוע כזה התוכנית כולה הסתיימה.

3. בלוק try ו except

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

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

try:
    x = int(input("Please select a first number: "))
    y = int(input("Please select a second number: "))

    print(f"{x} + {y} = {x + y}")
except ValueError:
    print("Sorry - you typed something that wasn't a number")

והתוצאה עכשיו נראית כך:

Please select a first number: a
Sorry - you typed something that wasn't a number

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

def read_number_stubbornly(prompt):
    while True:
        try:
            return int(input(prompt))

        except ValueError:
            print("Sorry - you typed something that wasn't a number. Try again")

x = read_number_stubbornly("Please select a first number: ")
y = read_number_stubbornly("Please select a second number: ")

print(f"{x} + {y} = {x + y}")

4. זריקת Exception

גם פונקציות שאנחנו כותבים יכולות לזרוק Exceptions. בשביל זה נרצה קודם להגדיר סוג חדש של Exception שיתאים למקרה המסוים שלנו. אנחנו מגדירים Exception חדש באמצעות ירושה וזה נראה כך:

class UglyNumberException(Exception):
    pass

נשתמש ב Exception החדשה שכתבנו כדי לדווח על שגיאה מתוך פונקציה. הפונקציה raise_unless_divides_by_3 שבדוגמא הבאה מקבלת מספר וזורקת את ה Exception שלנו אם המספר אינו מתחלק ב-3

def raise_unless_divides_by_3(num):
    if num % 3 != 0:
        raise UglyNumberException(f"The number {num} does not divide by 3")

# all ok
raise_unless_divides_by_3(9)

# blows up
raise_unless_divides_by_3(11)

בתיעוד על Exceptions תמצאו את כל סוגי ה Exceptions שפייתון תזרוק עליכם ומומלץ לקרוא גם אותו: https://docs.python.org/3/library/exceptions.html

כשאתם מפעילים פונקציות מערכת של Python זכרו שרובן זורקות Exceptions בכל מקרה שדברים לא עבדו בדיוק לפי התוכנית, ולכן תמיד מומלץ לבדוק בתיעוד איזה Exception פונקציה עשויה לזרוק, באיזה מקרים ולחשוב על הטיפול הנכון כשאנחנו כותבים את התוכניות שלנו.