May 5, 2021

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


פרק קודם:

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

מה נלמד

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

מהי שגיאה?



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

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

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

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

יש שגיאות שהמערכת נותנת לנו

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

קובץ לא קיים

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

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")

איזה try-except ייטפל בבעיה?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def A():
arr = [1,2,3]
a = arr[4]

def B():
try:
A()
print("B")
except ValueError as e:
print("B Exception")
def C():
try:
B()
except IndexError as e:
print("C Exception")

C()

אם אמרתם C Exception אז צדקתם!
למה? בתוך B יש לנו try-except אז איפה הבעיה?

הבעיה היא בסוג השגיאה שאנו תופסים - except ValueError as e.
מכיוון ש-B תופס שגיאה מסוג ValueError ולנו יש שגיאה של IndexError.
אז רק פונקציית C תתמודד עם השגיאה.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def A():
arr = [1,2,3]
a = arr[4]

def B():
try:
A()
print("B")
except ValueError as e:
print("B Exception")
def C():
try:
B()
except IndexError as e:
print("C Exception")

try:
C()
except:
print("General Exception")

בסוף הפוסט נדון בהרחבה יותר מה עדיף.

תרגיל

  1. תטפלו בשגיאה מסוג “קובץ לא קיים”.
1
file = open('C:\\text.txt','rt')
  1. בהינתן פונקציה שמבצעת חלוקה.
    תוסיפו try-except לפונקציה כך שבכל מקרה שגוי נחזיר 0 במקום לזרוק שגיאה.
1
2
3
4
5
6
def unsafe_divide(a,b):
return a / b

unsafe_divide(4, 2)
unsafe_divide(1, 0)
unsafe_divide(1,"B")

פתרונות

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

הטרמינולוגיה לשגיאות היא לזרוק או לעלות שגיאה.
מכיוון שבשפות אחרות המילה שמתשמשים בה היא 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}")

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

קוד שגיאה

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

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

מה הרציונל?

  • raise קופץ מחוץ לפונקציות כך שלעיתים נקפוץ מעל קטעי קוד שלמים כשאנו בודקים את התנאי שתופס.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def A():
raise Exception("A")

def B():
A()
print("B")

def C():
try:
B()
except Exception as e:
print(e)

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

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

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

השאלה המהותית ששואלים בהתחלה זה - “אוקיי יש לנו כמה דרכים, מהי הדרך הנכונה?”
אז התובנה הראשונה שלי זה - אין דרך נכונה - העיקר שתהיו עקביים בדרך שלכם.

יתרונות וחסרונות try-except

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

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

יתרונות חסרונות ב- tryXXX

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

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

יתרונות וחסרונות ב-קוד שגיאה

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

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

למשל:

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
49
50
51
52
53
54
def SaveFile(lines, path):
try:
with open(path, "wt") as file:
file.writelines(lines)
return 0
except Exception as e:
return 1

def TrySaveCache(data):
lines = []
for key in data:
lines.append(f"{key}={data[key]}\n")
successCode = SaveFile(lines, "StoredData.txt")
if successCode == 0:
return True
elif successCode== 1:
successCode = SaveFile(lines, "StoredData.txt") # Retry on error
return successCode == 0
else:
return False

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)

כאן מימשנו את שלושת התבניות.
פונקציה ה-save מחזירה error-code
ושאר הפונקציות מממשות try-except עם ה-tryXXX.

אם חושבים על הקוד הזה בצורה של שכבות:

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

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

מהסיבה הזו לעיתים נרצה רק להציג למשתמש שגיאה כמו “כן או לא”.


תרגילים

  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 - על הפונקציה לזרוק שגיאה!

  2. כתבו פונקציה read_file המקבלת נתיב לקובץ ומחזירה:
    0 ואת תוכן הקובץ במידה והכל בסדר.
    אם הקובץ לא קיים אז להחזיר 1 והסיבה לזה.
    אם אין לכם הרשאות יש להחזיר 2 עם הסיבה.
    אם זו סיבה אחרת וזה לא הצליח להחזיר 3.

שימו לב, ניתן להחזיר כמה פרמטרים בעזרת:

1
2
3
4
def myFunc():
return (1,"Hello World")

result, msg = myFunc()

זה נקרא desctruction.





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

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

על הפוסט

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

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#Software#Python