חריגות

הקדמה

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

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

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

הגדרה

חריגה (Exception) מייצגת כשל שהתרחש בזמן שפייתון ניסתה לפענח את הקוד שלנו או להריץ אותו.
כשהקוד קורס ומוצגת לנו הודעת שגיאה, אפשר להגיד שפייתון מתריעה על חריגה (raise an exception).

נבדיל בין שני סוגי חריגות: שגיאות תחביר וחריגות כלליות.

שגיאת תחביר

פייתון תתריע לפנינו על שגיאת תחביר (syntax error) כשנכתוב קוד שהיא לא מסוגלת לפענח.
לרוב זה יתרחש כשלא עמדנו בכללי התחביר של פייתון ושכחנו תו מסוים, בין אם זה סגירת סוגריים, גרשיים או נקודתיים בסוף שורה.
ודאי נתקלתם בשגיאה דומה בעבר:


In [ ]:
counter = 0
while counter < 10
    print("Stop it!")
    counter += 1

פייתון משתדלת לספק לנו כמה שיותר מידע על מקור השגיאה:

  1. בשורה הראשונה נראה מידע על מיקום השגיאה: קובץ (אם יש כזה) ומספר השורה שבה נמצאה השגיאה.
  2. בשורה השנייה את הקוד שבו פייתון מצאה את השגיאה.
  3. בשורה השלישית חץ שמצביע חזותית למקום שבו נמצאה השגיאה.
  4. בשורה הרביעית פייתון מסבירה לנו מה התרחש ומספקת הסבר קצר על השגיאה. במקרה הזה – SyntaxError.

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


In [ ]:
names = (
    "John Cleese",
    "Terry Gilliam",
    "Eric Idle",
    "Michael Palin",
    "Graham Chapman",
    "Terry Jones",

for name in names:
    print(name)

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

חריגה כללית

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

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


In [ ]:
a = int(input("Please enter the first number: "))
b = int(input("Please enter the second number: "))
print(a // b)

חשבו על כל החריגות שמשתמש שובב יכול לסחוט מהקוד שבתא למעלה.

חשוב!
פתרו לפני שתמשיכו!

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


In [ ]:
a = 5
b = 0
print(a // b)

פייתון התריעה בפנינו על ZeroDivisionError בשורה 3, וגם הפעם היא פלטה לנו הודעה מפורטת יחסית.
בדיוק כמו בשגיאת התחביר, השורה האחרונה היא הודעה שמספרת לנו מה קרה בפועל: חילקתם באפס וזה אסור.
באותה שורה נראה גם את סוג החריגה (ZeroDivisionError) – הקטגוריה הכללית שאליה החריגה משתייכת.

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


In [ ]:
a = int("5")
b = int("a")
print(a // b)

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

קריאת הודעת השגיאה

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


In [ ]:
a = int("5")
b = int("a")
print(a // b)

ננסה להבין לעומק את החלקים השונים של ההודעה:

איור המבאר את חלקי ההודעה המוצגת במקרים של התרעה על חריגה.

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


In [ ]:
def division(a, b):
    return int(a) // int(b)


division("meow", 5)

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

תחילה, נביט בשורה האחרונה ונקרא מה הייתה הסיבה שבגינה פייתון התריעה לנו על החריגה.
ההודעה היא invalid literal for int() with base 10: 'meow' – ניסינו להמיר את המחרוזת "meow" למספר שלם, וזה לא תקין.
כדאי להסתכל גם על סוג החריגה (ValueError) כדי לקבל מושג כללי על היצור שאנחנו מתעסקים איתו.

נמשיך ל־Traceback.
בפסקה שמעל השורה שבה מוצגת הודעת השגיאה, נסתכל על שורת הקוד שגרמה להתרעה על חריגה: return int(a) // int(b).
בשלב זה יש בידינו די נתונים לצורך פענוח ההתרעה על החריגה: ניסינו לבצע המרה לא חוקית של המחרוזת "meow" למספר שלם בשורה 2.

אם עדיין לא נחה דעתכם וקשה לכם להבין מאיפה הגיעה ההתרעה על החריגה, תוכלו להמשיך ולטפס במעלה ה־Traceback.
נעבור לקוד שגרם לשורה return int(a) // int(b) לרוץ: division("meow", 5).
נוכל לראות שהקוד הזה מעביר לפרמטר הראשון של הפונקציה division את הערך "meow", שאותו היא מנסה להמיר למספר שלם.
עכשיו ברור לחלוטין מאיפה מגיעה ההתרעה על החריגה.

טיפול בחריגות

הרעיון

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

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


In [ ]:
def get_file_content(filepath):
    with open(filepath) as file_handler:
        return file_handler.read()

ננסה לאחזר את התוכן של הקובץ castle.txt:


In [ ]:
princess_location = get_file_content('castle.txt')

במקרה שלמעלה ניסינו לפתוח קובץ שלא באמת קיים במחשב, ופייתון התריעה לנו על חריגת FileNotFoundError.
מכאן, שהפונקציה get_file_content עשויה לגרום להתרעה על חריגה מסוג FileNotFoundError בכל פעם שיועבר לה נתיב שאינו קיים.

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

התחביר הבסיסי

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

שימוש ב־try וב־except בפייתון נראה פחות או יותר כך:

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

נממש בקוד:


In [ ]:
def get_file_content(filepath):
    try:  # נסה לבצע את השורות הבאות
        with open(filepath) as file_handler:
            return file_handler.read()
    except FileNotFoundError:  # ...אם נכשלת בגלל סוג החריגה הזה, נסה לבצע במקום
        print(f"Couldn't open the file: {filepath}.")
        return ""

וננסה לאחזר שוב את התוכן של הקובץ castle.txt:


In [ ]:
princess_location = get_file_content("castle.txt")
princess_location

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

התחביר של try ... except הוא כדלהלן:

  1. נתחיל עם שורה שבה כתוב אך ורק try:.
  2. בהזחה, נכתוב את כל מה שאנחנו רוצים לנסות לבצע ועלול לגרום להתרעה על חריגה.
  3. בשורה הבאה, נצא מההזחה ונכתוב except ExceptionType:, כאשר ExceptionType הוא סוג החריגה שנרצה לתפוס.
  4. בהזחה (שוב), נכתוב קוד שנרצה לבצע אם פייתון התריעה על חריגה מסוג ExceptionType בזמן שהקוד המוזח תחת ה־try רץ.
התחביר של try ... except

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

ניקח את דוגמת הקוד שלמעלה, וננסה להבין כיצד פייתון קוראת אותה.
פייתון תתחיל בהרצת השורה with open("castle.txt") as file_handler: ותתריע על חריגה, משום שהקובץ castle.txt לא נמצא.
כיוון שהחריגה היא מסוג FileNotFoundError, היא תחפש את המילים except FileNotFoundError: מייד בסיום ההזחה.
הביטוי הזה קיים בדוגמה שלנו, ולכן פייתון תבצע את מה שכתוב בהזחה שאחריו במקום להתריע על חריגה.

תרשים זרימה המציג כיצד פייתון קוראת את הקוד במבנה try ... except

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

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

חשוב!
פתרו לפני שתמשיכו!

סוגים מרובים של חריגות

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


In [ ]:
princess_location = get_file_content("?")

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


In [ ]:
def get_file_content(filepath):
    try:
        with open(filepath) as file_handler:
            return file_handler.read()
    except FileNotFoundError:
        print(f"Couldn't open the file: {filepath}.")
        return ""

מכאן, נוכל לבחור לתקן את הקוד באחת משתי דרכים.

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


In [ ]:
def get_file_content(filepath):
    try:
        with open(filepath) as file_handler:
            return file_handler.read()
    except (FileNotFoundError, OSError):
        print(f"Couldn't open the file: {filepath}.")
        return ""

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

אבל מה אם נרצה שחריגות OSError תטופלנה בצורה שונה מחריגות FileNotFoundError?
במקרה הזה נפנה לדרך השנייה, שמימושה פשוט למדי – נוסף לקוד הקיים, נכתוב פסקת קוד חדשה שעושה שימוש ב־except:


In [ ]:
def get_file_content(filepath):
    try:
        with open(filepath) as file_handler:
            return file_handler.read()
    except FileNotFoundError:
        print(f"Couldn't open the file: {filepath}.")
        return ""
    except OSError:
        print(f"The path '{filepath}' is invalid.")
        return ""

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


In [ ]:
princess_location = get_file_content("?")
princess_location

לשמחתנו, אנחנו לא מוגבלים במספר ה־except־ים שאפשר להוסיף אחרי ה־try.

זיהוי סוג החריגה

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

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

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

  1. תיעוד – בשימוש בפונקציה או בפעולה מסוימת, קראו בתיעוד שלה על אילו חריגות עלולה פייתון להתריע בעקבות הפעלת הפונקציה או הפעולה.
  2. חיפוש – השתמשו במנוע חיפוש כדי לשאול אילו חריגות עלולות לקפוץ בעקבות פעולה כללית שביצעתם. נניח: python exceptions read file.
  3. נסו בעצמכם – אם ברצונכם לברר על סוג חריגה במקרה מסוים – הריצו את המקרה במחברת ובדקו על איזה סוג חריגה פייתון מתריעה.

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

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

חשוב!
פתרו לפני שתמשיכו!

תרגיל ביניים: תוכנית החלוקה

כתבו פונקציה בשם super_division שמקבלת מספר בלתי מוגבל של פרמטרים מספריים.

הפונקציה תבצע חלוקה של המספר הראשון במספר השני.
את התוצאה היא תחלק במספר השלישי לכדי תוצאה חדשה, את התוצאה החדשה היא תחלק במספר הרביעי וכן הלאה.
לדוגמה: עבור הקריאה super_division(100, 10, 5, 2) הפונקציה תחזיר 1, כיוון שתוצאת הביטוי $100 / 10 / 5 / 2$ היא 1.

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

פעפוע של חריגות במעלה שרשרת הקריאות

התרעה על חריגה גורמת לריצת התוכנית להתנהג בצורה שונה מעט ממה שהכרנו עד כה.

במהלך המחברת נתקלנו בשני מקרים אפשריים:

  1. או שהשורה שגרמה להתרעה על החריגה נמצאת ישירות תחת try-except שתואם לסוג החריגה, ואז החריגה נתפסת.
  2. או שהשורה הזו אינה נמצאת ישירות תחת try-except ואז החריגה מקריסה את התוכנית. במקרה שכזה, מוצג לנו Traceback.

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

נניח שבפונקציה A ישנה שורה שגרמה להתרעה על חריגה, והיא לא עטופה ב־try-except.
לפני שהתוכנית תקרוס, ההתרעה על החריגה תִּשָּׁלַח לפונקציה הקוראת, B, זו שהפעילה את פונקציה A שבה התרחשה ההתרעה על החריגה.
בשלב הזה פייתון נותנת לנו הזדמנות נוספת לתפוס את החריגה.
אם בתוך פונקציה B השורה שקראה לפונקציה A עטופה ב־try-except שתופס את סוג החריגה הנכונה, החריגה תטופל.
אם לא, החריגה תועבר לפונקציה C שקראה לפונקציה B, וכך הלאה, עד שנגיע לראש שרשרת הקריאות.
אם אף אחד במעלה שרשרת הקריאות לא תפס את החריגה, התוכנית תקרוס ויוצג לנו Traceback.

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


In [ ]:
def a():
    print("Dividing by zero...")
    return 1 / 0
    print("End of a.")


def b():
    print("Calling a...")
    a()
    print("End of b.")


def c():
    print("Calling b...")
    b()
    print("End of c.")


print("Start.")
print("Calling c...")
c()
print("Stop.")

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


In [ ]:
def a():
    print("Dividing by zero...")
    try:
        return 1 / 0
    except ZeroDivisionError:
        print("Never Dare Anyone to Divide By Zero!")
        print("https://reddit.com/2rkuek/")
    print("End of a.")


def b():
    print("Calling a...")
    a()
    print("End of b.")


def c():
    print("Calling b...")
    b()
    print("End of c.")


print("Start.")
print("Calling c...")
c()
print("Stop.")

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


In [ ]:
def a():
    print("Dividing by zero...")
    return 1 / 0
    print("End of a.")


def b():
    print("Calling a...")
    a()
    print("End of b.")


def c():
    print("Calling b...")
    try:
        b()
    except ZeroDivisionError:
        print("Never Dare Anyone to Divide By Zero!")
        print("https://reddit.com/2rkuek/")        
    print("End of c.")


print("Start.")
print("Calling c...")
c()
print("Stop.")

שימו לב שבמקרה הזה דילגנו על השורות שמדפיסות את ההודעה על סיום ריצתן של הפונקציות a ו־b.
בשורה מספר 3 התבצעה חלוקה לא חוקית ב־0 שגרמה להתרעה על חריגה מסוג ZeroDivisionError.
כיוון שהקוד לא היה עטוף ב־try-except, ההתרעה על החריגה פעפעה לשורה a() שנמצאת בפונקציה b.
גם שם אף אחד לא טיפל בחריגה באמצעות try-except, ולכן ההתרעה על החריגה המשיכה לפעפע לפונקציה שקראה ל־b, הלא היא c.
ב־c סוף כל סוף הקריאה ל־b הייתה עטופה ב־try-except, ושם התבצע הטיפול בחריגה.
מאותה נקודה שבה טופלה החריגה, התוכנית המשיכה לרוץ כרגיל.

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

תרגיל ביניים: החמישי זה נובמבר?

קראו את הקוד הבא, ופתרו את הסעיפים שאחריו.


In [12]:
MONTHS = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December",
]


def get_month_name(index):
    """Return the month name given its number."""
    return MONTHS[index - 1]


def get_month_number(name):
    """Return the month number given its name."""
    return MONTHS.index(name) + 1


def is_same_month(index, name):
    return (
        get_month_name(index) == name
        and get_month_number(name) == index
    )
  1. כתבו שתי שורות הקוראות ל־is_same_month, שאחת מהן מחזירה True והשנייה מחזירה False.
  2. על אילו חריגות פייתון עלולה להתריע בשתי הפונקציות הראשונות? תפסו אותן בפונקציות הרלוונטיות. החזירו None אם התרחשה התרעה על חריגה.
  3. בונוס: האם תצליחו לחשוב על דרך לגרום לפונקציה להתרסק בכל זאת? אם כן, תקנו אותה כך שתחזיר None במקרה שכזה.
  4. בונוס: האם תוכלו ליצור קריאה ל־is_same_month שתחזיר True בזמן שהיא אמורה להחזיר False?
  5. הבה נשנה גישה: תפסו את החריגות ברמת is_same_month במקום בפונקציות שהתריעו על חריגה. החזירו False אם התרחשה התרעה על חריגה.

הקשר בין חריגות למחלקות

כל התרעה על חריגה מיוצגת בפייתון באמצעות מופע.
בעזרת מילת המפתח as, נוכל ליצור משתנה שיצביע למופע הזה.

נכתוב אחרי ה־except ולפני הנקודתיים את הביטוי as VarName, כאשר VarName הוא שם משתנה חדש שיצביע למופע של ההתרעה על החריגה.
ביטוי זה יאפשר לנו לגשת למשתנה VarName שכולל את הפרטים על החריגה, מכל שורה שמוזחת תחת ה־except:


In [ ]:
try:
    1 / 0
except ZeroDivisionError as err:
    print(type(err))
    print('-' * 40)
    print(dir(err))

היופי במשתנה החדש שנוצר, err, זה שהוא מופע פייתוני שנוצר מתוך המחלקה ZeroDivisionError.
ZeroDivisionError, אם כך, היא מחלקה לכל דבר: יש לה __init__, פעולות ותכונות, כמו שאפשר לראות בדוגמת הקוד שלמעלה.

נבדוק אם יש לה __str__ מועיל:


In [ ]:
try:
    1 / 0
except ZeroDivisionError as err:
    print(f"The error is '{err}'.")

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

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


In [ ]:
ZeroDivisionError.mro()

וואו! זו שרשרת ירושות באורך שלא היה מבייש את שושלת המלוכה הבריטית.
אז נראה שהחריגה של חלוקה באפס (ZeroDivisionError) היא מקרה מיוחד של חריגה חשבונית.
חריגה חשבונית (ArithmeicError), בתורה, יורשת מ־Exception, שהיא עצמה יורשת מ־BaseException.

נביט במדרג הירושה המלא המוצג בתיעוד של פייתון כדי להתרשם:

BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StopAsyncIteration +-- ArithmeticError | +-- FloatingPointError | +-- OverflowError | +-- ZeroDivisionError <---- !הנה אנחנו +-- AssertionError +-- AttributeError +-- BufferError +-- EOFError +-- ImportError | +-- ModuleNotFoundError +-- LookupError | +-- IndexError | +-- KeyError +-- MemoryError +-- NameError | +-- UnboundLocalError +-- OSError | +-- BlockingIOError | +-- ChildProcessError | +-- ConnectionError | | +-- BrokenPipeError | | +-- ConnectionAbortedError | | +-- ConnectionRefusedError | | +-- ConnectionResetError | +-- FileExistsError | +-- FileNotFoundError | +-- InterruptedError | +-- IsADirectoryError | +-- NotADirectoryError | +-- PermissionError | +-- ProcessLookupError | +-- TimeoutError +-- ReferenceError +-- RuntimeError | +-- NotImplementedError | +-- RecursionError +-- SyntaxError | +-- IndentationError | +-- TabError +-- SystemError +-- TypeError +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError +-- Warning +-- DeprecationWarning +-- PendingDeprecationWarning +-- RuntimeWarning +-- SyntaxWarning +-- UserWarning +-- FutureWarning +-- ImportWarning +-- UnicodeWarning +-- BytesWarning +-- ResourceWarning

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


In [ ]:
try:
    1 / 0
except Exception as err:
    print(f"The error is '{err}'.")

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

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

בכל שימוש ב־try-except, צמצמו את כמות הקוד שנמצאת ב־try, וטפלו בחריגה כמה שיותר ספציפית.

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

סיכום

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

מונחים

חריגה (exception)
מופע המייצג מצב לא סדיר, לרוב בעייתי, שזוהה במהלך הרצת הקוד שבתוכנית.
לכל חריגה יש סוג, וכל סוג חריגה מיוצג באמצעות מחלקה פייתונית.
התרעה על חריגה (raise of an exception)
מצב שבו פייתון מודיעה על שגיאה או על מצב לא סדיר בריצת התוכנית.
התרעה על חריגה משנה את זרימת הריצה של התוכנית ועלולה לגרום לקריסתה.
Traceback
שרשרת הקריאות שהובילו להפעלתה של הפונקציה שבה אנחנו נמצאים ברגע מסוים.
בהקשר של חריגות, מדובר בשרשרת הקריאות שהובילה להתרעה על החריגה.
שרשרת הקריאות הזו מופיעה גם בהודעת השגיאה שמוצגת כאשר פייתון מתריעה על חריגה.
טיפול בחריגה (exception handling)
נקרא גם תפיסת חריגה (catching an exception).
בעת התרעה על חריגה, התנהגות ברירת המחדל של התוכנה היא קריסה.
מתכנת יכול להגדיר מראש מה הוא רוצה שיקרה במקרה של התרעה על חריגה.
במקרה כזה, קריסת התוכנית תימנע.

תרגילים

מחשבון

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

מנסה להבין איפה הסדר כאן

כתבו פונקציה בשם search_in_directory שמקבלת נתיב, ורשימה של מילות מפתח.
התוכנה תנסה לפתוח את כל הקבצים הנמצאים בנתיב, ותדפיס עבור כל מילת מפתח את כל הקבצים שבהם היא נמצאת.
התוכנה תרוץ גם על תתי־התיקיות שנמצאות בנתיב שסופק (ועל תתי־תתי התיקיות, וכן הלאה), אם יש כאלו.

לדוגמה:


In [ ]:
search_in_directory(r"C:\Projects\Notebooks\week8", ["class", "int"])

תדפיס את הפלט הבא:

---------------------------------------- class ---------------------------------------- C:\Projects\Notebooks\week8\1_Inheritance.ipynb C:\Projects\Notebooks\week8\2_Inheritance_Part_2.ipynb C:\Projects\Notebooks\week8\3_Exceptions.ipynb C:\Projects\Notebooks\week8\4_Exceptions_Part_2.ipynb C:\Projects\Notebooks\week8\images\exception_parts.svg ---------------------------------------- int ---------------------------------------- C:\Projects\Notebooks\week8\1_Inheritance.ipynb C:\Projects\Notebooks\week8\2_Inheritance_Part_2.ipynb C:\Projects\Notebooks\week8\3_Exceptions.ipynb C:\Projects\Notebooks\week8\4_Exceptions_Part_2.ipynb C:\Projects\Notebooks\week8\images\chessboard.svg C:\Projects\Notebooks\week8\images\diamond_problem.svg C:\Projects\Notebooks\week8\images\exception_parts.svg C:\Projects\Notebooks\week8\images\exception_propogation.svg C:\Projects\Notebooks\week8\images\inheritance.svg C:\Projects\Notebooks\week8\images\logo.jpg C:\Projects\Notebooks\week8\images\multilevel_inheritance.svg C:\Projects\Notebooks\week8\images\multiple_inheritance.svg C:\Projects\Notebooks\week8\images\multiple_inheritance.svg.old C:\Projects\Notebooks\week8\images\try_except_flow.svg C:\Projects\Notebooks\week8\images\try_except_syntax.svg

טפלו בכמה שיותר התרעות על חריגות שעלולות לצוץ במהלך ריצת התוכנית.