במחברת הקודמת התמודדנו לראשונה עם חריגות.
למדנו לפרק הודעות שגיאה לרכיביהן ולחלץ מהן מידע מועיל, העמקנו בדרך הפעולה של Traceback ודיברנו על סוגי החריגות השונים בפייתון.
ראינו לראשונה את מילות המפתח try
ו־except
, ולמדנו כיצד להשתמש בהן כדי לטפל בחריגות.
דיברנו על כך שטיפול בחריגות עשוי למנוע את קריסת התוכנית, וציינו גם שכדאי לבחור היטב באילו חריגות לטפל.
הבהרנו שאם נטפל בחריגות ללא אבחנה, אנחנו עלולים ליצור "תקלים שקטים" שפייתון לא תדווח לנו עליהם ויהיו קשים לאיתור.
לבסוף, הצגנו כיצד השגיאות בפייתון הן בסך הכול מופע שנוצר ממחלקה שמייצגת את סוג החריגה.
הראינו כיצד לקבל גישה למופע הזה מתוך ה־except
, וראינו את עץ הירושה המרשים של סוגי החריגות בפייתון.
במחברת זו נמשיך ללמוד על טיפול בחריגות.
עד סוף המחברת תוכלו להתריע בעצמכם על חריגה וליצור סוגי חריגות משל עצמכם.
זאת ועוד, תלמדו על יכולות מתקדמות יותר הנוגעות לטיפול בחריגות בפייתון, ועל הרגלי עבודה נכונים בכל הקשור בעבודה עם חריגות.
לעיתים חשוב לנו לוודא ששורת קוד תתבצע בכל מקרה, גם אם הכול סביב עולה באש.
לרוב, זה קורה כאשר אנחנו פותחים משאב כלשהו (קובץ, חיבור לאתר אינטרנט) וצריכים למחוק או לסגור את המשאב בסוף הפעולה.
במקרים כאלו, חשוב לנו שהשורה תתבצע אפילו אם הייתה התרעה על חריגה במהלך הרצת הקוד.
ננסה, לדוגמה, לכווץ את כל התמונות בתיקיית images לארכיון בעזרת המודול zipfile.
אין מה לחשוש – המודול מובן יחסית וקל לשימוש.
כל שנצטרך לעשות זה ליצור מופע של ZipFile ולהפעיל עליו את הפעולה write כדי לצרף לארכיון קבצים.
אם אתם מרגישים נוח, זה הזמן לכתוב את הפתרון לכך בעצמכם. אם לא, ודאו שאתם מבינים היטב את התאים הבאים.
נתחיל ביבוא המודולים הרלוונטיים:
In [ ]:
import os
import zipfile
וככלי עזר, נכתוב generator שמקבל כפרמטר נתיב לתיקייה, ומחזיר את הנתיב לכל הקבצים שבה:
In [ ]:
def get_file_paths_from_folder(folder):
"""Yield paths for all the files in `folder`."""
for file in os.listdir(folder):
path = os.path.join(folder, file)
yield path
עכשיו נכתוב פונקציה שיוצרת קובץ ארכיון חדש, מוסיפה אליו את הקבצים שבתיקיית התמונות וסוגרת את קובץ הארכיון:
In [ ]:
def zip_folder(folder_name):
our_zipfile = zipfile.ZipFile('images.zip', 'w')
for file in get_file_paths_from_folder(folder_name):
our_zipfile.write(file)
our_zipfile.close()
zip_folder('images')
אבל מה יקרה אם תיקיית התמונות גדולה במיוחד ונגמר לנו המקום בזיכרון של המחשב?
מה יקרה אם אין לנו גישה לאחד הקבצים והקריאה של אותו קובץ תיכשל?
נטפל במקרים שבהם פייתון תתריע על חריגה:
In [ ]:
def zip_folder(folder_name):
our_zipfile = zipfile.ZipFile('images.zip', 'w')
try:
for file in get_file_paths_from_folder(folder_name):
our_zipfile.write(file)
except Exception as error:
print(f"Critical failure occurred: {error}.")
our_zipfile.close()
zip_folder('NON_EXISTING_DIRECTORY')
התא למעלה מפר עיקרון חשוב שדיברנו עליו:
עדיף שלא לתפוס את החריגה אם לא יודעים בדיוק מה הסוג שלה, למה היא התרחשה וכיצד לטפל בה.
אבל רגע! אם לא נתפוס את החריגה, כיצד נוודא שהקוד שלנו סגר את קובץ הארכיון באופן מסודר לפני שהתוכנה קרסה?
זה הזמן להכיר את מילת המפתח finally
, שבאה אחרי ה־except
או במקומו.
השורות שכתובות ב־finally
יתבצעו תמיד, גם אם הקוד קרס בגלל חריגה.
שימוש ב־finally
ייראה כך:
In [ ]:
try:
1 / 0
finally:
print("+-----------------+")
print("| Executed anyway |")
print("+-----------------+")
שימו לב שאף על פי שהקוד שנמצא בתוך ה־try
קרס, ה־finally
התבצע.
למעשה, finally
עקשן כל כך שהוא יתבצע אפילו אם היה return
:
In [ ]:
def stubborn_finally_example():
try:
return True
finally:
print("This line will be executed anyway.")
stubborn_finally_example()
נשתמש במנגנון הזה כדי לוודא שקובץ הארכיון באמת ייסגר בסופו של דבר, ללא תלות במה שיקרה בדרך:
In [ ]:
def zip_folder(folder_name):
our_zipfile = zipfile.ZipFile('images.zip', 'w')
try:
for file in get_file_paths_from_folder(folder_name):
our_zipfile.write(file)
finally:
our_zipfile.close()
print(f"Is our_zipfiles closed?... {our_zipfile}")
zip_folder('images')
ונבדוק שזה יעבוד גם אם נספק תיקייה לא קיימת, לדוגמה:
In [ ]:
zip_folder('NO_SUCH_DIRECTORY')
יופי! עכשיו כשראינו התרעה על חריגת FileNotFoundError כשמשתמש הכניס נתיב לא תקין לתיקייה, ראוי שנטפל בה:
In [ ]:
def zip_folder(folder_name):
our_zipfile = zipfile.ZipFile('images.zip', 'w')
try:
for file in get_file_paths_from_folder(folder_name):
our_zipfile.write(file)
except FileNotFoundError as err:
print(f"Critical error: {err}.\nArchive is probably incomplete.")
finally:
our_zipfile.close()
print(f"Is our_zipfiles closed?... {our_zipfile}")
zip_folder('NO_SUCH_DIRECTORY')
יותר טוב!
היתרון בצורת הכתיבה הזו הוא שגם אם תהיה התרעה על חריגה שאינה מסוג FileNotFoundError והתוכנה תקרוס,
נוכל להיות בטוחים שקובץ הארכיון נסגר כראוי.
עד כה למדנו על 3 מילות מפתח שקשורות במנגנון לטיפול בחריגות של פייתון: try
, except
ו־finally
.
אלו רעיונות מרכזיים בטיפול בחריגות, ותוכלו למצוא אותם בצורות כאלו ואחרות בכל שפת תכנות עכשווית שמאפשרת טיפול בחריגות.
אלא שבפייתון ישנה מילת מפתח נוספת שהתגנבה למנגנון הטיפול בחריגות: else
.
תחת מילת המפתח הזו יופיעו פעולות שנרצה לבצע רק אם הקוד שב־try
רץ במלואו בהצלחה,
או במילים אחרות: באף שלב לא הייתה התרעה על חריגה; אף לא except
אחד התבצע.
In [ ]:
def read_file(path):
try:
princess = open(path, 'r')
except FileNotFoundError as err:
print(f"Can't find file '{path}'.\n{err}.")
return None
else:
text = princess.read()
princess.close()
return text
print(read_file('resources/castle.txt'))
"אבל רגע", ישאלו חדי העין מביניכם.
"הרי המטרה היחידה של else
היא להריץ קוד אם הקוד שב־try
רץ עד סופו,
אז למה שלא פשוט נכניס אותו כבר לתוך ה־try
, מייד אחרי הקוד שרצינו לבצע?"
וזו שאלה שיש בה היגיון רב –
הרי קוד שקורס ב־try
ממילא גורם לכך שהקוד שנמצא אחריו ב־try
יפסיק לרוץ.
אז למה לא פשוט לשים שם את קוד ההמשך? מה רע בקטע הקוד הבא?
In [ ]:
def read_file(path):
try:
princess = open(path, 'r')
text = princess.read()
princess.close()
return text
except FileNotFoundError as err:
print(f"Can't find file '{path}'.\n{err}.")
return None
print(read_file('resources/castle.txt'))
ההבדל הוא רעיוני בעיקרו.
המטרה שלנו היא להעביר את הרעיון שמשתקף מהקוד שלנו לקוראו בצורה נהירה יותר, קצת כמו בספר טוב.
מילת המפתח else
תעזור לקורא להבין איפה חשבנו שעשויה להיות ההתרעה על החריגה,
ואיפה אנחנו רוצים להמשיך ולהריץ קוד פייתון שקשור לאותו קוד.
ישנו יתרון נוסף בהפרדת הקוד ל־try
ול־else
–
השיטה הזו עוזרת לנו להפריד בין הקוד שבו ייתפסו התרעות על חריגות, לבין הקוד שירוץ אחריו ושבו לא יטופלו חריגות.
כיוון שהשורות שנמצאות בתוך ה־else
לא נמצאות בתוך ה־try
, פייתון לא תתפוס התרעות על חריגות שהתרחשו במהלך הרצתן.
שיטה זו עוזרת לנו ליישם את כלל האצבע שמורה לנו לתפוס התרעות על חריגות באופן ממוקד –
בעזרת else
לא נתפוס התרעות על חריגות בקוד שבו לא התכוונו מלכתחילה לתפוס התרעות על חריגות.
כתבו פונקציה בשם print_item שמקבלת כפרמטר ראשון רשימה, וכפרמטר שני מספר ($n$).
הפונקציה תדפיס את האיבר ה־$n$־י ברשימה.
טפלו בכל ההתרעות על חריגות שעלולות להיווצר בעקבות הרצת הפונקציה.
חשוב!
פתרו לפני שתמשיכו!
לסיכום, ניצור קטע קוד שמשתמש בכל מילות המפתח שלמדנו בהקשר של טיפול בחריגות:
In [ ]:
def read_file(path):
try:
princess = open(path, 'r')
text = princess.read()
except (FileNotFoundError, PermissionError) as err:
print(f"Can't find file '{path}'.\n{err}.")
text = None
else:
princess.close()
finally:
return text
print(read_file('resources/castle.txt3'))
כתבו פונקציה בשם estimate_read_time, שמקבלת נתיב לקובץ, ומודדת בתוך כמה זמן פייתון קוראת את הקובץ.
על הפונקציה להוסיף לקובץ בשם log.txt שורה שבה כתוב את שם הקובץ שניסיתם לקרוא, ובתוך כמה שניות פייתון קראה את הקובץ.
הפונקציה תטפל בכל מקרי הקצה ובהתרעות על חריגות שבהם היא עלולה להיתקל.
עד כה התמקדנו בטיפול בהתרעות על חריגות שעלולות להיווצר במהלך ריצת התוכנית.
בהגיענו לכתוב תוכניות גדולות יותר שמתכנתים אחרים ישתמשו בהן, לעיתים קרובות נרצה ליצור בעצמנו התרעות על חריגות.
התרעה על חריגה, כפי שלמדנו, היא דרך לדווח למתכנת שמשהו בעייתי התרחש בזמן ריצת התוכנית.
נוכל ליצור התרעות כאלו בעצמנו כדי להודיע למתכנתים שמשתמשים בקוד שכתבנו, על בעיות אפשריות.
יצירת התרעה על חריגה היא עניין פשוט למדי שמורכב מ־3 חלקים:
raise
.זה ייראה כך:
In [ ]:
raise ValueError("Just an example.")
נראה דוגמה לקוד אמיתי שמממש התרעה על חריגה.
הקוד הבא לקוח מהמודול datetime, והוא רץ בכל פעם שמבקשים ליצור מופע חדש של תאריך.
שימו לב כיצד יוצר המודול בודק את כל אחד מחלקי התאריך, ואם הערך חורג מהטווח שהוגדר – הוא מתריע על חריגה עם הודעת חריגה ממוקדת:
In [ ]:
def _check_time_fields(hour, minute, second, microsecond, fold):
if not 0 <= hour <= 23:
raise ValueError('hour must be in 0..23', hour)
if not 0 <= minute <= 59:
raise ValueError('minute must be in 0..59', minute)
if not 0 <= second <= 59:
raise ValueError('second must be in 0..59', second)
if not 0 <= microsecond <= 999999:
raise ValueError('microsecond must be in 0..999999', microsecond)
if fold not in (0, 1):
raise ValueError('fold must be either 0 or 1', fold)
return hour, minute, second, microsecond, fold
מטרת הפונקציה היא להבין אם השעה שהועברה ל־datetime תקינה.
בפונקציה, בודקים אם השעה היא מספר בטווח 0–23, אם מספר הדקות הוא מספר בטווח 0–59 וכן הלאה.
אם אחד התנאים לא מתקיים – מתריעים למתכנת שניסה ליצור את מופע התאריך על חריגה.
הקוד משתמש בתעלול מבורך – ביצירת מופע ממחלקה של חריגה, אפשר להשתמש ביותר מפרמטר אחד.
הפרמטר הראשון תמיד יוקדש להודעת השגיאה, אבל אפשר להשתמש בשאר הפרמטרים כדי להעביר מידע נוסף על החריגה.
בדרך כלל מעבירים שם מידע על הערכים שגרמו לבעיה, או את הערכים עצמם.
בתור רשת לכלי עבודה אתם מנסים לספור את מלאי הסולמות, כרסומות ומחרטות שקיימים אצלכם.
כתבו מחלקה שמייצגת חנות (Store), ולה 3 תכונות:
מספר הסולמות (ladders), מספר הכרסומות (millings) ומספר המחרטות (lathes) במלאי.
כתבו פונקציה בשם count_inventory שמקבלת רשימת מופעים של חנויות, ומחזירה את מספר הפריטים הכולל במלאי.
צרו התרעות על חריגות במידת הצורך, בין אם במחלקה ובין אם בפונקציה.
טכניקה מעניינת שמשתמשים בה מדי פעם היא ניסוח מחדש של התרעה על חריגה.
נבחר לנהוג כך כשהניסוח מחדש יעזור לנו למקד את מי שישתמש בקוד שלנו.
בטכניקה הזו נתפוס בעזרת try
חריגה מסוג מסוים, וב־except
ניצור התרעה חדשה על חריגה עם הודעת שגיאה משלנו.
נראה דוגמה:
In [ ]:
DAYS = [
'Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday',
]
def get_day_by_number(number):
try:
return DAYS[number - 1]
except IndexError:
raise ValueError("The number parameter must be between 1 and 7.")
for i in range(1, 9):
print(get_day(i))
טכניקה נוספת היא ביצוע פעולות מסוימות במהלך ה־except
, והתרעה על החריגה מחדש.
השימוש בטכניקה הזו נפוץ מאוד.
שימוש בה הוא מעין סיפור קצר בשלושה חלקים:
לדוגמה:
In [ ]:
ADDRESS_BOOK = {
'Padfoot': '12 Grimmauld Place, London, UK',
'Jerry': 'Apartment 5A, 129 West 81st Street, New York, New York',
'Clark': '344 Clinton St., Apt. 3B, Metropolis, USA',
}
def get_address_by_name(name):
try:
return ADDRESS_BOOK[name]
except KeyError as err:
with open('errors.txt', 'w') as errors:
errors.write(str(err))
raise KeyError(str(err))
for name in ('Padfoot', 'Clark', 'Jerry', 'The Ink Spots'):
print(get_address_by_name(name))
למעשה, הרעיון של התרעה מחדש על חריגה הוא כה נפוץ, שמפתחי פייתון יצרו עבורו מעין קיצור.
אם אתם נמצאים בתוך except
ורוצים לזרוק בדיוק את החריגה שתפסתם, פשוט כתבו raise
בלי כלום אחריו:
In [ ]:
def get_address_by_name(name):
try:
return ADDRESS_BOOK[name]
except KeyError as err:
with open('errors.txt', 'w') as errors:
errors.write(str(err))
raise
for name in ('Padfoot', 'Clark', 'Jerry', 'The Ink Spots'):
print(get_address_by_name(name))
בתוכנות גדולות במיוחד נרצה ליצור סוגי חריגות משלנו.
נוכל לעשות זאת בקלות אם נירש ממחלקה קיימת שמייצגת חריגה:
In [ ]:
class AddressUnknownError(Exception):
pass
בשלב זה, נוכל להתריע על חריגה בעזרת סוג החריגה שיצרנו:
In [ ]:
def get_address_by_name(name):
try:
return ADDRESS_BOOK[name]
except KeyError:
raise AddressUnknownError(f"Can't find the address of {name}.")
for name in ('Padfoot', 'Clark', 'Jerry', 'The Ink Spots'):
print(get_address_by_name(name))
זכרו שהירושה כאן משפיעה על הדרך שבה תטופל החריגה שלכם.
אם, נניח, AddressUnknownError הייתה יורשת מ־KeyError, ולא מ־Exception,
זה אומר שכל מי שהיה עושה except KeyError
היה תופס גם חריגות מסוג AddressUnknownError.
יש לא מעט יתרונות ליצירת שגיאות משל עצמנו:
כבכל ירושה, תוכלו לדרוס את הפעולות __init__
ו־__str__
של מחלקת־העל שממנה ירשתם.
דריסה כזו תספק לכם גמישות רבה בהגדרת החריגות שיצרתם ובשימוש בהן.
נראה דוגמה קצרצרה ליצירת חריגה מותאמת אישית:
In [339]:
class DrunkUserError(Exception):
"""Exception raised for errors in the input."""
def __init__(self, name, bac, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = name
self.bac = bac # Blood Alcohol Content
def __str__(self):
return (
f"{self.name} must not drriiiive!!! @_@"
f"\nBAC: {self.bac}"
)
def start_driving(username, blood_alcohol_content):
if blood_alcohol_content > 0.024:
raise DrunkUserError(username, blood_alcohol_content)
return True
start_driving("Kipik", 0.05)
טיפול בחריגות היא הדרך הטובה ביותר להגיב על התרחשויות לא סדירות ולנהל אותן בקוד הפייתון שאנחנו כותבים.
כפי שכבר ראינו במחברות קודמות, בכלים מורכבים ומתקדמים יש יותר מקום לטעויות, וקווים מנחים יעזרו לנו להתנהל בצורה נכונה.
נעבור על כמה כללי אצבע ורעיונות מועילים שיקלו עליכם לעבוד נכון עם חריגות:
באופן כללי, נעדיף להיות כמה שיותר ממוקדים בטיפול בחריגות.
כשאנחנו מטפלים בחריגה, אנחנו יוצאים מנקודת הנחה שאנחנו יודעים מה הבעיה וכיצד יש לטפל בה.
לדוגמה, אם משתמש הזין ערך שלא נתמך בקוד שלנו, נרצה לעצור את קריסת התוכנית ולבקש ממנו להזין ערך מתאים.
לא נרצה, לדוגמה, לתפוס התרעות על חריגות שלא התכוונו לתפוס מלכתחילה.
אנחנו מעוניינים לטפל רק בבעיות שאנחנו יודעים שעלולות להתרחש.
אם ישנה בעיה שאנחנו לא יודעים עליה – אנחנו מעדיפים שפייתון תצעק כדי שנדע שהיא קיימת.
"השתקה" של בעיות שאנחנו לא יודעים על קיומן היא פתח לתקלים בלתי צפויים וחמורים אף יותר.
בקוד, הנקודה הזו תבוא לידי ביטוי כשנכתוב אחרי ה־except
את רשימת סוגי החריגות שבהן נטפל.
נשתדל שלא לטפל ב־Exception, משום שאז נתפוס כל סוג חריגה שיורש ממנה (כמעט כולם).
נשתדל גם לא לדחוס אחרי ה־except
סוגי חריגות שאנחנו לא יודעים אם הם רלוונטיים או לא.
יתרה מזאת, טיפול בשגיאות יתבצע רק על קוד שאנחנו יודעים שעלול לגרום להתרעה על חריגה.
קוד שלא קשור לחריגה שהולכת להתרחש – לא יהיה חלק מהליך הטיפול בשגיאות.
בקוד, הנקודה הזו תבוא לידי ביטוי בכך שבתוך ה־try
יוזחו כמה שפחות שורות קוד.
תחת ה־try
נכתוב אך ורק את הקוד שעלול להתריע על חריגה, ושום דבר מעבר לו.
כך נדע שאנחנו לא תופסים בטעות חריגות שלא התכוונו לתפוס מלכתחילה.
אנחנו מעוניינים שהמתכנת שישתמש בקוד יקבל התרעות על חריגות שיבהירו לו מהן הבעיות בקוד שכתב, ויאפשרו לו לטפל בהן.
אם כתבנו מודול או פונקציה שמתכנת אחר הולך להשתמש בה, לדוגמה, נקפיד ליצור התרעות על חריגות שיעזרו לו לנווט בקוד שלנו.
לעומת המתכנת, אנחנו שואפים שמי שישתמש בתוכנית (הלקוח של המוצר, נניח) לעולם לא יצטרך להתמודד עם התרעות על חריגות.
התוכנית לא אמורה לקרוס בגלל חריגה אף פעם, אלא לטפל בחריגה ולחזור לפעולה תקינה.
אם החריגה קיצונית ומחייבת את הפסקת הריצה של התוכנית, עלינו לפעול בצורה אחראית:
נבצע שמירה מסודרת של כמה שיותר פרטים על הודעת השגיאה, נסגור חיבורים למשאבים, נמחק קבצים שיצרנו ונכבה את התוכנה בצורה מסודרת.
בכל הקשור לשפות תכנות, ישנן שתי גישות נפוצות לטיפול במקרי קצה בתוכנית.
הגישה הראשונה נקראת LBYL, או Look Before You Leap ("הסתכל לפני שאתה קופץ").
גישה זו דוגלת בבדיקת השטח לפני ביצוע כל פעולה.
הפעולה תתבצע לבסוף, רק כשנהיה בטוחים שהרצתה חוקית ולא גורמת להתרעה על חריגה.
קוד שכתב מי שדוגל בשיטה הזו מתאפיין בשימוש תדיר במילת המפתח if
.
הגישה השנייה נקראת EAFP, או Easier to Ask for Forgiveness than Permission ("קל יותר לבקש סליחה מלבקש רשות").
גישה זו דוגלת בביצוע פעולות מבלי לבדוק לפני כן את היתכנותן, ותפיסה של התרעה על חריגה אם היא מתרחשת.
קוד שכתב מי שדוגל בשיטה הזו מתאפיין בשימוש תדיר במבני try-except
.
נראה שתי דוגמאות להבדלים בגישות.
נכתוב פונקציה שמקבלת מחרוזת ומיקום ($n$), ומחזירה את התו במיקום ה־$n$־י במחרוזת.
לפניכם הקוד בגישת LBYL, ובו אנחנו מנסים לבדוק בזהירות אם אכן מדובר במחרוזת, ואם יש בה לפחות $n$ תווים.
רק אחרי שאנחנו מוודאים שכל דרישות הקדם מתקיימות, אנחנו ניגשים לבצע את הפעולה.
In [ ]:
def get_nth_char(string, n):
n = n - 1 # string[0] is the first char (n = 1)
if isinstance(string, (str, bytes)) and n < len(string):
return string[n]
return ''
print(get_nth_char("hello", 1))
והנה אותו קוד בגישת EAFP. הפעם פשוט ננסה לאחזר את התו, ונסמוך על מבנה ה־try-except
שיתפוס עבורנו את החריגות:
In [ ]:
def get_nth_char(string, n):
try:
return string[n + 1]
except (IndexError, TypeError) as e:
print(e)
return ''
נתכנת פונקציה שמקבלת נתיב לקובץ ולטקסט, וכותבת את הטקסט לקובץ.
הנה הקוד בגישת LBYL, ובו אנחנו מנסים לבדוק בזהירות אם הקובץ אכן בטוח לכתיבה.
רק אחרי שאנחנו מוודאים שיש לנו גישה אליו, שאכן מדובר בקובץ ושאפשר לכתוב אליו, אנחנו מבצעים את הכתיבה לקובץ.
In [ ]:
import os
import pathlib
def is_path_writeble(filepath):
"""Return if the path is writeable."""
path = pathlib.Path(filepath)
directory = path.parent
is_dir_writeable = directory.is_dir() and os.access(directory, os.W_OK)
is_exists = path.exists()
is_file_writeable = path.is_file() and os.access(path, os.W_OK)
return is_dir_writeable and ((not is_exists) or is_file_writeable)
def write_textfile(filepath, text):
"""Safely write `text` to `filepath`."""
if is_path_writeble(filepath):
with open(filepath, 'w', encoding='utf-8') as f:
f.write(text)
return True
return False
write_textfile("fo", "class")
והנה אותו קוד בגישת EAFP. הפעם פשוט ננסה לכתוב לקובץ, ונסמוך על מבנה ה־try-except
שיתפוס עבורנו את החריגות:
In [ ]:
import os
import pathlib
def write_textfile(filepath, text):
"""Safely write `text` to `filepath`."""
try:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(text)
except (FileNotFoundError, IsADirectoryError, PermissionError) as e:
print(e)
return False
return True
write_textfile("fo", "class")
מתכנתי פייתון נוטים יותר לתכנות בגישת EAFP.
טיפול בחריגה ימנע מהתוכנה לקרוס, ועשוי להחביא את העובדה שהייתה בעיה בזרימת התוכנית.
לרוב זה מצוין ובדיוק מה שאנחנו רוצים, אבל מתכנתים בתחילת דרכם עלולים להתפתות לנצל את העובדה הזו יתר על המידה.
לפניכם דוגמה לקטע קוד שחניכים רבים משתמשים בו בתחילת דרכם:
In [ ]:
try:
# Code
...
except Exception:
pass
הטריק הזה נקרא "השתקת חריגות".
ברוב המוחלט של המקרים זה לא מה שאנחנו רוצים.
השתקת החריגה עלולה לגרום לתקל בהמשך ריצת התוכנית, ויהיה לנו קשה מאוד לאתר אותו בעתיד.
פעמים רבות השתקה שכזו מעידה על כך שהחריגה נתפסה מוקדם מדי.
במקרים כאלו, עדיף לטפל בהתרעה על החריגה בפונקציה שקראה למקום שבו התרחשה ההתרעה על החריגה.
אם תגיעו למצב שבו אתם משתיקים חריגות, עצרו ושאלו את עצמכם אם זה הפתרון הטוב ביותר.
לרוב, עדיף יהיה לטפל בהתרעה על החריגה ולדאוג להביא את התוכנה למצב תקין,
או לפחות לשמור את פרטי ההתרעה לקובץ המתעד את ההתרעות על החריגות שהתרחשו בזמן ריצת התוכנה.
לפניכם דוגמאות קוד מחרידות להפליא.
תקנו אותן כך שיתאימו לנימוסים והליכות שלמדנו בסוף המחברת.
היעזרו באינטרנט במידת הצורך.
In [ ]:
# Example 1
class PhoneNumberNotFound(Exception):
pass
In [ ]:
# Example 2
def get_key(d, k, default=None):
try:
return d[k]
except:
return default
In [ ]:
# Example 3
def write_file(path, text):
try:
f = open(path, 'w')
f.write(text)
f.close()
except IOError:
pass
In [ ]:
# Example 4
PHONEBOOK = {'867-5309': 'Jenny'}
def get_name_by_phone(phonebook, phone_number):
if phone_number not in phonebook:
raise ValueError("person_number not in phonebook")
return phonebook[phone_number]
phone_number = input("Hi Mr. User!\nEnter phone:")
get_name_by_phone(PHONEBOOK, phone_number)
In [ ]:
# Example 5
def my_sum(items):
try:
total = 0
for element in items:
total = total + element
return total
except TypeError:
return 0
כתבו פונקציה המיועדת למתכנתים בחברת "The Syndicate".
הפונקציה תקבל כפרמטרים נתיב לקובץ (filepath) ומספר שורה (line_number).
הפונקציה תחזיר את מה שכתוב בקובץ שנתיבו הוא filepath בשורה שמספרה הוא line_number.
נהלו את השגיאות היטב. בכל פעם שישנה התרעה על חריגה, כתבו אותה לקובץ log.txt עם חותמת זמן וההודעה.
צפנת פענח ניסה להעביר ליוליוס פואמה מעניינת שכתב.
בניסיוננו להתחקות אחר עקבותיו של צפנת פענח, ניסינו לשים את ידינו על המסר – אך גילינו שהוא מוצפן.
בתיקיית resources מצורפים שני קבצים: users.txt ו־passwords.txt.
כל שורה בקובץ users.txt נראית כך:
העמודה הראשונה מייצגת את מספר המשתמש, העמודה השנייה מייצגת את שמו ושאר העמודות מייצגות פרטים מזהים עליו.
העמודות מופרדות בתו |.
כל שורה בקובץ בקובץ passwords.txt נראית כך:
שתי העמודות הראשונות הן מספרי המשתמש, כפי שהם מוגדרים ב־users.txt.
העמודה השלישית היא סיסמת ההתקשרות ביניהם.
כתבו את הפונקציות הבאות:
לצורך פתרון החידה, מצאו את סיסמת ההתקשרות של המשתמשים Zaphnath Paaneah ו־Gaius Iulius Caesar.
פענחו בעזרתה את המסר הסודי שבקובץ message.txt.
השתמשו בתרגיל כדי לתרגל את מה שלמדתם בנושא טיפול בחריגות.
In [ ]:
def digest(key, data):
S = list(range(256))
j = 0
for i in list(range(256)):
j = (j + S[i] + ord(key[i % len(key)])) % 256
S[i], S[j] = S[j], S[i]
j = 0
y = 0
for char in data:
j = (j + 1) % 256
y = (y + S[j]) % 256
S[j], S[y] = S[y], S[j]
yield chr(ord(char) ^ S[(S[j] + S[y]) % 256])
def decrypt(key, message):
return ''.join(digest(key, message))