Exceptions ג': raise — לזרוק שגיאה בעצמנו

מטרות השיעור
  • לזרוק Exception בעצמנו עם raise והודעה ברורה
  • לבחור סוג שגיאה מתאים — ValueError, TypeError ועוד
  • להבין את החלוקה: הפונקציה מזהה בעיה וזורקת, מי שקרא מחליט איך לטפל
  • להעביר שגיאה הלאה מתוך except עם raise בלבד

מה נלמד?

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

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

נניח שכתבתם פונקציה withdraw(balance, amount) למשיכה מחשבון בנק. מה היא צריכה לעשות כשמבקשים למשוך מינוס 500? אפשרויות:

  1. לבצע בשקט — היתרה גדלה ב-500. באג כספי חמור.
  2. להדפיס “error” — אבל מי שקרא לפונקציה לא יודע שהיא נכשלה, וממשיך לעבוד עם תוצאה שגויה.
  3. להחזיר ערך מיוחד (כמו -1 או None) — מי שקרא חייב לזכור לבדוק אותו. אם שכח, הבאג “זולג”.
  4. לזרוק שגיאה — מי שקרא חייב להתייחס אליה, או שהתוכנית תעצור עם הודעה ברורה בדיוק במקום הבעייתי.

אפשרות 4 היא הדרך המקצועית. בנושא OOP נשתמש בה ל-Validation בתוך מחלקות, ובנושא Backend ומסדי נתונים שרת FastAPI יהפוך שגיאות כאלה לתשובת HTTP 400 או 404.

הסבר מהבסיס

raise

raise זורקת Exception, בדיוק כמו ש-Python עושה כשמשהו נשבר. מרגע ה-raise:

  • הפונקציה מפסיקה מיד. שום שורה אחריה לא רצה, ושום ערך לא מוחזר.
  • השגיאה “עולה” למי שקרא לפונקציה. אם הוא תופס אותה עם except, מצוין. אם לא, היא ממשיכה לעלות, ובסוף התוכנית קורסת עם Traceback.

החלוקה הנכונה: מי מזהה ומי מטפל

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

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

איזה סוג שגיאה לזרוק?

משתמשים בסוגים המובנים, לפי משמעות:

סוגמתי
ValueErrorהטיפוס נכון, אבל הערך לא חוקי: גיל שלילי, סכום אפס, מייל בלי @
TypeErrorהטיפוס לא נכון: התקבל טקסט במקום מספר
KeyError / LookupErrorחיפוש של משהו שלא קיים
PermissionErrorאין הרשאה לפעולה

ההודעה צריכה להסביר מה לא בסדר ולמה: "amount must be positive, got -500", ולא "error".

(בנושא OOP, אחרי שנלמד מחלקות, נראה איך יוצרים סוגי שגיאה משלנו, כמו InsufficientFundsError.)

תחביר

raise ValueError("clear message about what is wrong")

# typical pattern: validate at the start of a function
def function_name(value):
    if not is_valid(value):
        raise ValueError("why it is invalid")
    # ... normal work, only with valid data

# the caller decides
try:
    function_name(bad_value)
except ValueError as error:
    print("Problem:", error)

# re-raise: handle partly, then pass the same error up
except ValueError:
    print("logging the problem...")
    raise

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

week-2/withdraw.py:

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount


print(withdraw(1000, 300))
print(withdraw(1000, -500))
print("this line never runs")

פלט:

700
Traceback (most recent call last):
  File "withdraw.py", line 10, in <module>
    print(withdraw(1000, -500))
  File "withdraw.py", line 3, in withdraw
    raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -500

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

שורההסבר
if amount <= 0:Validation (אימות קלט): בודקים לפני שעושים משהו.
raise ValueError("...")יוצרים אובייקט ValueError עם הודעה, וזורקים אותו. הפונקציה נעצרת כאן.
if amount > balance:בדיקה שנייה, לכלל עסקי אחר.
return balance - amountמגיעים לכאן רק אם כל הבדיקות עברו. מכאן והלאה, הנתונים בטוחים.
withdraw(1000, 300)עובר את הבדיקות ומחזיר 700.
withdraw(1000, -500)נזרקת שגיאה. אף אחד לא תופס אותה, ולכן התוכנית קורסת עם Traceback ברור. השורה האחרונה בקוד לא רצה.

שימו לב לאיכות ה-Traceback: ההודעה אומרת בדיוק מה הבעיה (amount must be positive) ואיזה ערך גרם לה (-500). כך נראית שגיאה טובה.

דוגמה נוספת

מי שקרא מחליט. אותה פונקציה, עם קוד שתופס ומטפל. week-2/atm_safe.py:

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount


balance = 1000
requests = [200, -50, 5000, 300]

for amount in requests:
    try:
        balance = withdraw(balance, amount)
    except ValueError as error:
        print("Rejected:", error)
    else:
        print("Withdrew", amount, "- new balance:", balance)

print("Final balance:", balance)

פלט:

Withdrew 200 - new balance: 800
Rejected: amount must be positive, got -50
Rejected: insufficient funds: balance 800, requested 5000
Withdrew 300 - new balance: 500
Final balance: 500

פירוק:

  • withdraw לא מדפיסה כלום ולא יודעת מה יקרה אחר כך. היא רק מזהה וזורקת.
  • הלולאה (מי שקרא) מחליטה: הבקשה נדחית עם הודעה, והלולאה ממשיכה לבקשה הבאה.
  • כשהשגיאה נזרקת, ההשמה balance = withdraw(...) לא מתבצעת, ולכן היתרה נשארת כמו שהייתה. זה בדיוק מה שרצינו.
  • מחר אותה withdraw יכולה לשמש באתר, באפליקציה או בשרת, וכל אחד מהם יטפל בשגיאה בדרך שלו (הודעה על המסך, תשובת HTTP 400…).

raise בלי שום דבר אחריו — העברה הלאה

לפעמים רוצים לעשות משהו כשיש שגיאה (למשל לרשום אותה), אבל לא “להעלים” אותה:

try:
    balance = withdraw(balance, 5000)
except ValueError:
    print("Audit: a failed withdrawal attempt was recorded")
    raise

raise לבדו, בתוך except, זורק מחדש את אותה שגיאה בדיוק. היא ממשיכה לעלות למי שמעל.

טעות נפוצה

הטעות 1: print במקום raise.

def withdraw(balance, amount):
    if amount > balance:
        print("Not enough money!")
    return balance - amount

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

הטעות 2: הודעה לא מועילה.

raise ValueError("error")
raise ValueError("bad input")

איזה input? מה היה הערך? מה מצופה? הודעה טובה = מה התנאי + מה התקבל: "age must be between 0 and 120, got 150".

הטעות 3: לזרוק את הסוג הכללי ביותר.

raise Exception("amount must be positive")

Exception הוא אב של כמעט כל השגיאות. מי שירצה לתפוס רק את הבעיה הזו, יצטרך except Exception, שתופס גם באגים לא קשורים. בחרו סוג ספציפי (ValueError).

הטעות 4: לתפוס ולזרוק, ולאבד את ההקשר.

except ValueError:
    raise ValueError("something failed")

ההודעה המקורית (עם הפרטים) הלכה לאיבוד. אם רוצים להעביר הלאה, raise לבד שומר על השגיאה המקורית.

תרגול

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

תרגיל 1: גיל תקין

כתבו פונקציה set_age(age) שמחזירה את הגיל אם הוא בין 0 ל-120, ואחרת זורקת ValueError עם ההודעה שבפלט הצפוי (כולל הגיל שהתקבל). אחר כך כתבו לולאה שמנסה את הגילים [25, -3, 150]: לגיל תקין היא מדפיסה ok: ואת הגיל, ולשגיאה — error: ואת ההודעה.

פלט צפוי:

ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
הצג פתרון
MIN_AGE = 0
MAX_AGE = 120


def set_age(age):
    if age < MIN_AGE or age > MAX_AGE:
        raise ValueError("age must be between 0 and 120, got " + str(age))
    return age


for age in [25, -3, 150]:
    try:
        print("ok:", set_age(age))
    except ValueError as error:
        print("error:", error)
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150

הפונקציה רק מזהה את הבעיה וזורקת. מי שקרא לה — הלולאה — מחליט מה לעשות.

תרגיל 2: שם משתמש

כתבו פונקציה validate_username(name) שזורקת ValueError:

  • אם השם קצר מ-3 תווים — עם ההודעה too short;
  • אם יש בו רווח — עם ההודעה no spaces allowed.

אם השם תקין — היא מחזירה אותו.

for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)

פלט צפוי:

valid: dana
al -> too short
big boss -> no spaces allowed
הצג פתרון
MIN_USERNAME_LENGTH = 3


def validate_username(name):
    if len(name) < MIN_USERNAME_LENGTH:
        raise ValueError("too short")
    if " " in name:
        raise ValueError("no spaces allowed")
    return name


for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
valid: dana
al -> too short
big boss -> no spaces allowed

תרגיל 3: מי שקרא — מחליט

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

def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100

כתבו שתי פונקציות שמשתמשות בה בשתי דרכים שונות:

  1. report_price(price) — מדפיסה את המחיר אחרי 10% הנחה, או invalid price אם הייתה שגיאה.
  2. price_or_zero(price) — מחזירה את המחיר אחרי 10% הנחה, או 0 אם הייתה שגיאה.
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))

פלט צפוי:

180.0
invalid price
45.0
0
הצג פתרון
def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100


def report_price(price):
    try:
        print(apply_discount(price, 10))
    except ValueError:
        print("invalid price")


def price_or_zero(price):
    try:
        return apply_discount(price, 10)
    except ValueError:
        return 0


report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
180.0
invalid price
45.0
0

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

תרגיל 4: return None מול raise

שתי גרסאות של אותה פונקציה:

# A                                   # B
def withdraw(balance, amount):        def withdraw(balance, amount):
    if amount > balance:                  if amount > balance:
        print("Not enough money!")            raise ValueError("not enough money")
        return None                       return balance - amount
    return balance - amount

מישהו כתב: balance = withdraw(100, 500). מה יקרה בכל גרסה, ולמה B עדיפה?

הצג פתרון
  • A: מודפס “Not enough money!”, ו-balance מקבל None. התוכנית ממשיכה עם יתרה None, ותקרוס מאוחר יותר במקום אחר — רחוק מהבעיה האמיתית.
  • B: נזרקת ValueError בדיוק במקום של הבעיה. אם הקורא לא טיפל בה, ה-Traceback מצביע ישר על המשיכה הבעייתית. אם טיפל — הוא מחליט בעצמו מה להציג.
  • B גם לא מדפיסה בעצמה: אולי הקורא הוא שרת שצריך להחזיר הודעה, ולא להדפיס למסך.

תרגיל 5: המרת ציון

כתבו פונקציה parse_grade(text) שממירה טקסט לציון. היא זורקת ValueError אם הטקסט אינו מספר (זה קורה כבר ב-int), או אם הציון מחוץ לטווח 0 עד 100 — עם ההודעה שבפלט הצפוי (כולל הציון שהתקבל).

for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)

פלט צפוי:

grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
הצג פתרון
def parse_grade(text):
    grade = int(text)
    if grade < 0 or grade > 100:
        raise ValueError("grade must be 0-100, got " + str(grade))
    return grade


for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120

parse_grade לא תופסת את השגיאה של int — היא נותנת לה לעבור הלאה לקורא, בדיוק כמו את השגיאה שהיא זורקת בעצמה.

סיכום

  • raise SomeError("message") זורק שגיאה. הפונקציה נעצרת מיד, ושום ערך לא מוחזר.
  • הפונקציה מזהה וזורקת. מי שקרא תופס ומחליט.
  • ValueError לערך לא חוקי, TypeError לטיפוס לא נכון. לא Exception כללי.
  • הודעה טובה = מה הכלל + מה התקבל.
  • raise לבד בתוך except מעביר את אותה שגיאה הלאה.
  • לזרוק עדיף על להדפיס “error” או להחזיר ערך מיוחד, כי שגיאה אי אפשר “לשכוח לבדוק”.

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