לדלג לתוכן

3.4 טיפול בשגיאות הרצאה

שגיאות

  • שגיאות הן בעיות לא צפויות שיכולות לקרות בזמן שהתוכנה רצה ולגרום לה לקרוס.
  • סוגי שגיאות: בפייתון יש המון סוגים של שגיאות, כגון: TypeError, ValueError, ZeroDivisionError, וכו.
    def divide_numbers(a, b):
        return a / b
    
    divide_numbers(5, 0) # Will raise an expcetion 
    
  • בדוגמה למעלה הקוד יזרוק שגיאת ZeroDivisionError.

  • סטאק טרייס: (stack trace) כאשר שגיאה קורת, פייתון מייצרת סטאק טרייס- שזה דוח שמציין את הדברים הבאים:

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

    def func2(a):
        # Doing something...
        0 / a  # Will raise an exception
    
    def func1(a, b):
        # Doing something..
        func2(a)
        return 
    
    func(1, 2)
    

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

  • למשל, אם פונקציה שכתבנו קיבלה פרמטר לא צפוי, אולי נרצה להחזיר שגיאה יזומה.
    def func(x):
        if x < 0:
            raise ValueError("x should be a non-negative number")
    
    func(-5)
    
  • הדרך הטובה ביותר לדאוג לכך שנעביר פרמטרים נכונים לפונקציות שלנו ולא נצור בעיות היא באמצעות זריקת שגיאות:
    def divide_numbers(a, b):
        if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
            raise TypeError("Both inputs should be numbers")
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b
    
  • הקוד למעלה בודק האם הפרמטרים שהועברו לפונקציה הם מספרים, ושמכנה הוא לא 0.
  • מטרת הבדיקות האלו היא שהפונקציה לא תזרוק שגיאה, אלה אם כן הפרמטרים שהועברו לא נכונים.

הבעיה בשגיאות

  • שגיאות מקריסות את התוכנה, תוכנה טובה לא קורסת.
  • פתרון: "טיפול בשגיאות" או באנגלית "exception handling" זה גישה תכנותית למנוע שגיאות.

טיפול בשגיאות

  • כאשר אנחנו כותבים פונקציות שעלולת לזרוק שגיאות, נכתוב פונקציות שמטרתם לתפוס את השגיאות ולטפל בהם במקום להקריס את התוכנה. נעשה זאת באמצעות try ו - execpt
    def divide_numbers(a, b):
        return a / b
    
    def divide_operation(first_num, second_num):
        try:
            result = divide_numbers(first_num, second_num) # This will maybe raise a ZeroDivisionError
        except ZeroDivisionError:
            print("Cannot divide by zero!")
            return
    
        print("Operation succeeded")
        return result
    
    
    divide_operation(10, 0) # Program won't crash :)
    
  • הפונקציה divide_numbers היא פונקציה שעשוייה לקרוס ולהחזיר שגיאות, אז נעטוף אותה עם הפונקציה divide_operation שמטרתה לקרוא לה, ולדאוג שאם היא תחזיר שגיאה אז לטפל בה במקום להקריס את התוכנה.
  • הבלוק קוד שנמצא בtry זה בלוק שאם קורת בו שגיאה מסוג ZeroDivisionError הבלוק except יורץ.

  • מה אם נרצה לתפוס עוד סוגים של שגיאות שעלולות לקרות? למשל ValueError? (כאשר הוכנס מחרוזת לפונקציה), נוכל לעשות כמה בלוקים של except.

    def divide_numbers(a, b):
        return a / b
    
    def divide_operation(first_num, second_num):
        try:
            result = divide_numbers(first_num, second_num) # This will maybe raise a ZeroDivisionError
        except ZeroDivisionError:
            print("Cannot divide by zero!")
            return
    
        except ValueError:
            print("Can only divide numbers!")
            return
    
        print("Operation succeeded")
        return result
    
    
    divide_operation("sdsda", 3) # Program won't crash :)
    

  • אם השגיאה תהיה ZeroDivisionError אז הבלוק של הexcept הראשון יורץ, אם השגיאה תהיה ValueError, אז הבלוק של הexcept השני יורץ.

  • ומה אם תקרה שגיאה כללית שאנחנו לא בהכרח מכירים? איך נוכל לתפוס כל שגיאה אחרת? באמצעות except Exception

    def divide_numbers(a, b):
        return a / b
    
    def divide_operation(first_num, second_num):
        try:
            result = divide_numbers(first_num, second_num) # This will maybe raise a ZeroDivisionError
        except ZeroDivisionError:
            print("Cannot divide by zero!")
            return
    
        except ValueError:
            print("Can only divide numbers!")
            return
    
        except Exception as e:
            print(f"An error occurred: {e}")
            return
    
        print("Operation succeeded")
        return result
    
    
    divide_operation(10, 0) # Program won't crash :)
    

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

  • במידה ונרצה להריץ בלוק של קוד אחרי try ו - execpt שירוץ לא משנה אם קרתה שגיאה או לא, נשתמש בfinally

    def divide_numbers(a, b):
        return a / b
    
    def divide_operation(first_num, second_num):
        try:
            result = divide_numbers(first_num, second_num)  # This may raise a ZeroDivisionError
        except ZeroDivisionError:
            print("Cannot divide by zero!")
            return
        except Exception as e:
            print(f"An error occurred: {e}")
            return
    
        print("Operation succeed")
        return result
    
        finally:
            print("Division operation complete.")  # This block will always execute, regardless of exceptions
    
    
    result = divide_operation(10, 2)  # Operation succeed, Division operation complete.
    print(result)  # Output: 5.0
    

  • לא משנה אם התוכנה קרסה או לא, תמיד הבלוק finally יורץ.
  • אפשר לראות שאם לא הייתה שגיאה קיים עוד בלוק של קוד שירוץ, (נמצא בין הexcept Exception ל- finally) אפשר להשתמש בelse כדי לתחם אותו בצורה ברורה יותר.
    def divide_numbers(a, b):
        return a / b
    
    def divide_operation(first_num, second_num):
        try:
            result = divide_numbers(first_num, second_num)  # This may raise a ZeroDivisionError
        except ZeroDivisionError:
            print("Cannot divide by zero!")
        except Exception as e:
            print(f"An error occurred: {e}")
        else:
            print("Operation succeed")
            return result
        finally:
            print("function finished.")  # This block will always execute, regardless of exceptions
    
    result = divide_operation(10, 2)  # Operation succeed, Division operation complete.
    print(result)  # Output: 5.0
    
  • שימו לב שהקוד הבא הוא זהה לקוד הקודם.

משפחות של שגיאות

  • בתמונה הבאה קיימים המון שגיאות בפייתון.
    Pasted image 20240204160329.png
  • שימו לב שגם קיימים משפחות של שגיאות: (שגיאה שמצביעה על שגיאות אחרות)
  • למשל ArithmeticError הוא כולל בתוכו גם את ZeroDivisionError וגם OverflowError.
  • אם נעשה except ArithmeticError אנחנו נתפוס את כל משפחת השגיאות הזו.
  • בנוסף, אפשר לראות בתמונה שכל השגיאות הם במשפחה של Exception, וזו הסיבה שאם נעשה except Exception אנחנו נתפוס את כל השגיאות.

https://docs.python.org/3/library/exceptions.html#exception-hierarchy

בדיקת קלט vs טיפול בשגיאות

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

def handle_division(number1, number2):
    try:
        return number1 / number2
    except Exception:
        print(f"An error occurred.")
        return

while True:
    number1 = input("enter first number: ")
    number2 = input("enter second number: ")
    handle_division(number1, number2)

- בדוגמה למעלה וידאתי שהפרמטרים שהועברו לפונקציה handle_division נכונים באמצעות try, except

  1. דרך שנייה - באמצעות בדיקת קלט
    def handle_division(number1, number2):
        return number1 / number2
    
    def get_input():
        while True:
            number1 = input("enter first number: ")
            number2 = input("enter second number: ")
            if isinstance(number1, int) and isinstance(number2, int):
                return number1, number2
            else:
                print("numbers can be only integers!")
    
    while True:
        number1, number2 = get_input()
        handle_division(number1, number2)
    
  2. בדוגמה למעלה וידאתי שהפרמטרים שהועברו לפונקציה handle_division נכונים באמצעות תנאים (בדיקת קלט).

  3. למעשה תמיד עדיף שילוב של שניהם, כך אתם יכולים לוודא שהקוד שלכם יהיה כמה שיותר נכון ובטוח.

    def handle_division(number1, number2):
        try:
            return number1 / number2
        except Exception:
            print(f"An error occurred.")
            return
    
    def get_input():
        while True:
            number1 = input("enter first number: ")
            number2 = input("enter second number: ")
            if isinstance(number1, int) and isinstance(number2, int):
                return number1, number2
            else:
                print("numbers can be only integers!")
    
    while True:
        number1, number2 = get_input()
        handle_division(number1, number2)
    

  4. כך אנחנו גם בודקים את הinput,
    if isinstance(number1, int) and isinstance(number2, int):
        return number1, number2
    else:
        print("numbers can be only integers!")
    
  5. וגם תופסים שגיאות:
    try:
        return number1 / number2
    except Exception:
        print(f"An error occurred.")
        return
    
  6. אל תכתבו רק try ו - except, תמיד תכתבו גם בדיקות קלט כמו בדוגמה, אל תסתמכו אף פעם על הקלט שהמשתמש מכניס לתוכנה שלכם.