Exceptions ג': raise — לזרוק שגיאה בעצמנו
- לזרוק Exception בעצמנו עם
raiseוהודעה ברורה - לבחור סוג שגיאה מתאים — ValueError, TypeError ועוד
- להבין את החלוקה: הפונקציה מזהה בעיה וזורקת, מי שקרא מחליט איך לטפל
- להעביר שגיאה הלאה מתוך
exceptעםraiseבלבד
מה נלמד?
עד עכשיו Python זרקה שגיאות, ואנחנו תפסנו. היום נלמד לזרוק בעצמנו, עם raise, כשהקוד שלנו מזהה מצב לא חוקי.
למה צריך את זה?
נניח שכתבתם פונקציה withdraw(balance, amount) למשיכה מחשבון בנק. מה היא צריכה לעשות כשמבקשים למשוך מינוס 500? אפשרויות:
- לבצע בשקט — היתרה גדלה ב-500. באג כספי חמור.
- להדפיס “error” — אבל מי שקרא לפונקציה לא יודע שהיא נכשלה, וממשיך לעבוד עם תוצאה שגויה.
- להחזיר ערך מיוחד (כמו
-1אוNone) — מי שקרא חייב לזכור לבדוק אותו. אם שכח, הבאג “זולג”. - לזרוק שגיאה — מי שקרא חייב להתייחס אליה, או שהתוכנית תעצור עם הודעה ברורה בדיוק במקום הבעייתי.
אפשרות 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יכולה לשמש באתר, באפליקציה או בשרת, וכל אחד מהם יטפל בשגיאה בדרך שלו (הודעה על המסך, תשובת HTTP400…).
raise בלי שום דבר אחריו — העברה הלאה
לפעמים רוצים לעשות משהו כשיש שגיאה (למשל לרשום אותה), אבל לא “להעלים” אותה:
try:
balance = withdraw(balance, 5000)
except ValueError:
print("Audit: a failed withdrawal attempt was recorded")
raiseraise לבדו, בתוך 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כתבו שתי פונקציות שמשתמשות בה בשתי דרכים שונות:
report_price(price)— מדפיסה את המחיר אחרי 10% הנחה, אוinvalid priceאם הייתה שגיאה.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 120parse_grade לא תופסת את השגיאה של int — היא נותנת לה לעבור הלאה לקורא, בדיוק כמו את השגיאה שהיא זורקת בעצמה.
סיכום
raise SomeError("message")זורק שגיאה. הפונקציה נעצרת מיד, ושום ערך לא מוחזר.- הפונקציה מזהה וזורקת. מי שקרא תופס ומחליט.
ValueErrorלערך לא חוקי,TypeErrorלטיפוס לא נכון. לאExceptionכללי.- הודעה טובה = מה הכלל + מה התקבל.
raiseלבד בתוךexceptמעביר את אותה שגיאה הלאה.- לזרוק עדיף על להדפיס “error” או להחזיר ערך מיוחד, כי שגיאה אי אפשר “לשכוח לבדוק”.
דוקומנטציה רשמית
- Python Tutorial — Raising Exceptions — raise
- The raise statement — הגדרת raise בשפה