א.14 — חריגות — טיפול בשגיאות בזמן ריצה

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

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

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

מחברות האוניברסיטה הפתוחה — 7-חריגות

try ו־except

המבנה try ו־except מאפשר להגדיר מה לעשות כשפעולה נכשלת באופן צפוי. בתוך try כותבים את הפעולה שעלולה להיכשל. except מציינת את סוג החריגה שבה נטפל, ואחריה כותבים את הקוד שירוץ רק אם החריגה אכן נוצרה. אם הכול עבר בהצלחה, גוף ה־except מדולג. למשל, int("abc") גורמת ל־ValueError, החריגה שמציינת ערך לא מתאים:

try:
    number = int("abc")
except ValueError:
    print("Please enter an integer")

פלט

Please enter an integer

מבקשים קלט מחדש

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

while True:
    try:
        number = int(input("Integer: "))
        break
    except ValueError:
        print("Invalid integer")
print("You entered:", number)

רק המרה מוצלחת מגיעה ל־break. אם נוצרה חריגה, הזרימה עוברת מיד ל־except ואז לאיטרציה הבאה. לדוגמה, אחרי קלט w ואחריו 3:

פלט

Integer: w
Invalid integer
Integer: 3
You entered: 3

סוגי כשל שונים

פעולה אחת יכולה להיכשל מכמה סיבות שונות, ולכל סיבה מתאים טיפול אחר. סוג החריגה מתאר מה השתבש, וכך אפשר להתאים את הטיפול לכשל. ZeroDivisionError מציינת חלוקה באפס; TypeError פעולה עם טיפוס שאינו מתאים; IndexError גישה לאינדקס שאינו קיים. תופסים את הסוג הצפוי במקום להסתיר כל כשל באותה הודעה. בדוגמה הבאה בלוק try אחד מלווה בכמה בלוקי except, אחד לכל סוג כשל, ובבלוק else שמופעל רק כשהחלוקה הצליחה:

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
    except TypeError:
        print("Numbers are required")
    else:
        print("Result:", result)

divide(10, 2)
divide(10, 0)

פלט

Result: 5.0
Cannot divide by zero

הקריאה divide(10, 2) מסתיימת בהצלחה ולכן מודפסת התוצאה; הקריאה divide(10, 0) יוצרת ZeroDivisionError ונתפסת בבלוק המתאים. else מתבצעת אם גוף ה־try הסתיים ללא חריגה. יש גם בלוק finally, המיועד לפעולת סיום וניקוי שצריכה להתבצע בכל מקרה, גם כשנוצרה חריגה, למשל סגירת קובץ. בעבודה עם קבצים נלמד דרך נוחה לסגירה באמצעות with.

תפיסת כל החריגות — Exception

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

def divide(a, b):
    try:
        result = a / b
    except Exception as err:
        print("Error: ", err)
    else:
        print("Result:", result)

divide(10, 1)
divide(10, 0)
divide("as", 5)

פלט

Result: 10.0
Error:  division by zero
Error:  unsupported operand type(s) for /: 'str' and 'int'

הקריאה השנייה יוצרת ZeroDivisionError והשלישית TypeError, ושתיהן נתפסות באותו בלוק; ההודעה המודפסת היא זו שפייתון צירפה לחריגה. תפיסה גורפת כזו נוחה כשרוצים רק לדווח על הכשל ולהמשיך, אבל היא מסתירה גם כשלים שלא ציפינו להם, ובהם באגים בקוד שלנו. לכן כשיודעים מה עלול להיכשל תופסים את הסוג הצפוי, ומשתמשים ב־Exception כרשת ביטחון אחרונה. דרך ביניים היא לציין כמה סוגים בבלוק אחד באמצעות סוגריים: except (ZeroDivisionError, TypeError) as err: תופסת רק את שני הסוגים האלה.

יצירת חריגה באמצעות raise

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

def positive_age(age):
    if age < 0:
        raise ValueError("Age must be non-negative")
    return age

try:
    positive_age(-2)
except ValueError as error:
    print(error)

פלט

Age must be non-negative

חריגה שלא נתפסה בפונקציה עוברת לקוד שקרא לה, וכך הלאה במעלה שרשרת הקריאות, עד שמישהו תופס אותה או עד שהתוכנית נעצרת. כאן החריגה נוצרה בתוך positive_age ונתפסה ב־except שמחוץ לה. as error נותנת שם לאובייקט החריגה, כפי שראינו, וכאן ההודעה שלו היא זו שנכתבה בסוגריים של raise.