May 5, 2021

פייתון 13 - שגיאות


פרק קודם:

JSON פייתון 12.2 - קבצי

מה נלמד

  • מהי שגיאה
  • איך מטפלים בשגיאות
  • איך זורקים שגיאות
  • מה כדאי, לבדוק או לשגות?

מהי שגיאה?



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

שגיאות בפייתון

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

קובץ לא קיים

במידה ונכתוב קוד שקורא קבצים, יכול לקרות מצב שקובץ לא יהיה קיים:

1
2
with open('c:\\fileDoesntExists.txt','rt') as file: 
pass

שגיאה בהמרה

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

1
2
a = "abc"
b = int(a)

תרגיל

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

מה קורה אם אנחנו לא מטפלים בשגיאות?

קוד שלא נמצא מוגן יפסיק לרוץ כי שגיאה כזו היא בטרמינולוגיה נקראת “שגיאה פטאלית” - Fatal Error.
שגיאה שהתוכנה שלנו לא יכולה להתאושש ממנו ולכן היא חייבת להיכבה.

במערכות הפעלה קיימות גם שגיאות כאלו כמו ה-Blue Screen שיש בווינדוס.
במקרה שהתוכנה שלנו מפסיקה לרוץ ניתן לומר שהיא “קורסת” או “מפסיקה את הריצה”.

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

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

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

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

1
2
3
4
5
try:
list = [1,2,3]
list[4]
except:
print(e)

כל מה שבתוך ה-try יהיה מוגן משגיאות.
במקרה ותקרה שגיאה מכל סוג שהיא, קטע הקוד בתוך ה-except ירוץ.

טיפול פרטני בשגיאה

עבור שגיאה פרטנית ניתן למנות את סוג השגיאה,
למשל בשגיאה הקודמת יש לנו שגיאה מסוג IndexError.
"as e" נותן שם לשגיאה שנוכל להשתמש בו בתור משתנה.

1
2
3
4
5
6
7
try:
list = [1,2,3]
list[4]
except IndexError as e:
print(e)
finally:
print("Final")

finally - גם אם לא קרתה שגיאה

ניתן להשתמש ב-finally גם אם לא קרתה שגיאה:

1
2
3
4
5
6
7
try:
list = [1,2,3]
list[2]
except IndexError as e:
print(e)
finally:
print("Final")

ניתן להשתמש בקוד שנמצא ב-finally כדי לבצע עבודות ניקוי במידה ורוצים.

1
2
3
4
5
6
7
8
list = [1,2,3,4,5]
try:
a = list[7]
except:
print("Err")
finally:
list.clear()
print("Clear")

finally לא מטפל בשגיאות בפני עצמו:

1
2
3
4
5
6
list = [1,2,3,4,5]
try:
a = list[7]
finally:
list.clear()
print("Clear")

תרגיל

  1. תטפלו בשגיאה מסוג “קובץ לא קיים”.
    1
    file = open('C:\\text.txt','rt')

איך זורקים שגיאות

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

לזרוק שגיאה כללית

1
2
3
4
a = input('Enter your username: ')

if(len(a) < 6):
raise Exception(f"Username {a} is too short")

אם שם המשתמש שלנו קצר מדי אז נזרוק שגיאה מתאימה עם ההודעה: Username ____ is too short.

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

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

1
2
3
4
5
6
7
8
9
10
def WriteErrorToLog(error):
with open('app.log','at') as file:
file.write(str(error))

try:
a = 'abc'
b = int(a)
except Exception as e:
WriteErrorToLog(e)
raise e

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

אסור “לבלוע” שגיאות שאנחנו לא מטפלים בהם

למשל קטע הקוד הזה פסול:

1
2
3
4
5
6
7
def DoSomethingBig():
raise ValueError()

try:
DoSomethingBig()
except:
pass

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

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

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

ההחלטה אם לטפל בשגיאות או לבדוק את הקלט היא החלטה עיצובית

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

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

1
2
3
4
# Ignore the ValueError that can be thrown from int()
a = int(input('Pick a divider'))
b = 5 / a
print(f"Result is {b}")

אם המשתמש יכניס 0 - הקוד הזה יפסיק לרוץ בגלל שגיאה.
ניתן לטפל בשגיאה הזו בשני דרכים:

  1. לטפל בשגיאה

    1
    2
    3
    4
    5
    6
    7
    a = int(input('Pick a divider'))
    b = 0
    try:
    b = 5 / a
    print(f"Result is {b}")
    except:
    print(f"Error a is {a}")
  2. לבדוק אם זה 0.

    1
    2
    3
    4
    5
    6
    a = int(input('Pick a divider'))
    if a == 0:
    print("Cannot divide by 0")
    else:
    b = 5 / a
    print(f"Result is {b}")

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

Try Pattern

תבנית נפוצה שבולעת שגיאות היא ה - TryXXX pattern.
בתבנית הזו אנחנו מנסים לבצע פעולה ומחזירים שני ערכים:

  1. אם הצלחנו או לא.
  2. את ערך ההחזרה שרצינו להחזיר.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def TryParseInt(stringToParse):
try:
return int(stringToParse), True
except ValueError:
return stringToParse, False

a = input("Pick a divider")
output, success = TryParseInt(a)

if success and output != 0:
b = 5 / output
print(f"Result is {b}")
else:
print(f"Cannot divid by picked input {a}")

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

ההבדל העיצובי

ההבדל בין תבנית Try - True/False ל-try except הוא כמות הבדיקות.
בד”כ כשמחזירים משתנה בוליאני שאומר לנו אם הפעולה הצליחה או לא, נרצה לעלות את התוצאה הזו למעלה בהיררכיה של הפונקציות.

למשל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def TrySaveFile(lines, path):
try:
with open(path, "wt") as file:
file.writelines(lines)
return True
except:
return False

def TrySaveCache(data):
lines = []
for key in data:
lines.append(f"{key}={data[key]}\n")
success = TrySaveFile(lines, "StoredData.txt")
return success

def TryReadFile(path):
try:
lines = []
with open(path, "rt") as file:
lines = file.readlines()
return lines, True
except:
return [], False

def TryLoadCache():
data = {}
lines, success = TryReadFile("StoredData.txt")
if success:
for line in lines:
kv = line.split("=")
data[kv[0]] = kv[1]
return data, True
else:
return data, False


storedData, success = TryLoadCache()
if not success:
print("Cache couldn't be loaded, exiting...")
exit()

aInput = input("Command: ")
while aInput.lower() != "exit":
kv = aInput.split("=")
storedData[kv[0]] = kv[1]
aInput = input("Command: ")

TrySaveCache(storedData)

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

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

מתכנתים הם לא קופי קוד - עליהם לדעת באיזו דרך כדאי יותר לבחור


תרגילים

  1. עליכם לבנות מחשבון פשוט התומך בחיבור, חיסור כפל או חילוק של מספרים של שני מספרים.
    ניתן לקבל כקלט:
    1+1
    2-3
    10*2
    99/3

עליכם לטפל בשגיאות שיכולות להיווצר - על המחשבון להיות רובסטי (Robust).
השתמשו בתבנית TryXXX.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def TryCalculate(userInput):
pass

result, success = TryCalculate("1+1")
if success and result == 2:
print("Test 1 passed")

result2, success2 = TryCalculate("6/0")

if not success2:
print("Test 2 passed")

result3, success3 = TryCalculate("5*20")
if success3 and result3 == 100:
print("Test 3 passed")

result4, success4 = TryCalculate("abdcas")
if not success4:
print("Test 4 passed")

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




בפרק הבא נבנה קוד כמו אמאזון - בחבילות!

פייתון 14 - מודולים

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Python