2.26. זריקת שגיאות

פונקציה יכולה לאותת על בעיה לקורא שלה על ידי זריקת חריגה. מילת המפתח היא raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

קריאה ל-square_root(-1) עוצרת בשורת ה-raise, קופצת החוצה מהפונקציה, ומחפשת except תואם בקורא. אם אף קורא לא תופס אותה, הסקריפט מסתיים עם traceback.

2.26.1. מדוע לזרוק במקום להחזיר ערך-זקיף (sentinel)

שתי דרכים לדווח על ”קלט שגוי“:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

צורת החריגה בדרך כלל טובה יותר:

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

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

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

השתמשו בערכי-זקיף רק כאשר ”לא נמצא“ הוא תוצאה שגרתית ולא-חריגה – dict.get() מחזירה None על מפתח חסר בדיוק משום שצפוי שחיפושים יחמיצו לעיתים.

2.26.2. מחלקות חריגה מותאמות אישית

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

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

גוף ה-class הריק הוא בסדר – השם עצמו הוא מה שחשוב, משום שקוראים תופסים לפי מחלקה. קבצו שגיאות קשורות תחת בסיס משותף אם קורא עשוי לרצות לתפוס את כל המשפחה בבלוק אחד.

2.26.2.1. זריקה מחדש

raise חשוף בתוך בלוק except זורק מחדש את החריגה הנוכחית כך שהיא מתפשטת למטפל הבא:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

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

2.26.3. מתי לתפוס ומתי להפיץ

כלל אצבע שימושי:

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

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

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