חריגות – חלק 2

הקדמה

במחברת הקודמת התמודדנו לראשונה עם חריגות.
למדנו לפרק הודעות שגיאה לרכיביהן ולחלץ מהן מידע מועיל, העמקנו בדרך הפעולה של 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'))
תרשים זרימה המציג כיצד פייתון קוראת את הקוד במבנה try, except, else, finally.

תרגיל ביניים: פותחים שעון

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

יצירת התרעה על חריגה

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

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

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

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

זה ייראה כך:


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, והתרעה על החריגה מחדש.
השימוש בטכניקה הזו נפוץ מאוד.

שימוש בה הוא מעין סיפור קצר בשלושה חלקים:

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

לדוגמה:


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))
נהוג לסיים את שמות המחלקות המייצגות חריגה במילה Error.

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

יש לא מעט יתרונות ליצירת שגיאות משל עצמנו:

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

כבכל ירושה, תוכלו לדרוס את הפעולות __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)


---------------------------------------------------------------------------
DrunkUserError                            Traceback (most recent call last)
<ipython-input-339-c133afbec7d4> in <module>
     20 
     21 
---> 22 start_driving("Kipik", 0.05)

<ipython-input-339-c133afbec7d4> in start_driving(username, blood_alcohol_content)
     16 def start_driving(username, blood_alcohol_content):
     17     if blood_alcohol_content > 0.024:
---> 18         raise DrunkUserError(username, blood_alcohol_content)
     19     return True
     20 

DrunkUserError: Kipik must not drriiiive!!! @_@
BAC: 0.05

נימוסים והליכות

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

טיפול ממוקד

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

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

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

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

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

חריגות הן עבור המתכנת

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

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

EAFP או LBYL

בכל הקשור לשפות תכנות, ישנן שתי גישות נפוצות לטיפול במקרי קצה בתוכנית.

הגישה הראשונה נקראת LBYL, או Look Before You Leap ("הסתכל לפני שאתה קופץ").
גישה זו דוגלת בבדיקת השטח לפני ביצוע כל פעולה.
הפעולה תתבצע לבסוף, רק כשנהיה בטוחים שהרצתה חוקית ולא גורמת להתרעה על חריגה.
קוד שכתב מי שדוגל בשיטה הזו מתאפיין בשימוש תדיר במילת המפתח if.

הגישה השנייה נקראת EAFP, או Easier to Ask for Forgiveness than Permission ("קל יותר לבקש סליחה מלבקש רשות").
גישה זו דוגלת בביצוע פעולות מבלי לבדוק לפני כן את היתכנותן, ותפיסה של התרעה על חריגה אם היא מתרחשת.
קוד שכתב מי שדוגל בשיטה הזו מתאפיין בשימוש תדיר במבני try-except.

נראה שתי דוגמאות להבדלים בגישות.

דוגמה 1: מספר תו במחרוזת

נכתוב פונקציה שמקבלת מחרוזת ומיקום ($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 ''

דוגמה 2: כתיבה לקובץ

נתכנת פונקציה שמקבלת נתיב לקובץ ולטקסט, וכותבת את הטקסט לקובץ.

הנה הקוד בגישת 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 נראית כך:

1|Jesse Henderson|lowejohn@gmail.com|95675 Debra Canyon Apt. 862 Port Jeremy, MD 16600|2000-10-15 21:50:07|Digitized exuding knowledge user

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

כל שורה בקובץ בקובץ passwords.txt נראית כך:

0|0|i8gvD1!pOIPiLvOY5W72yZU9C#

שתי העמודות הראשונות הן מספרי המשתמש, כפי שהם מוגדרים ב־users.txt.
העמודה השלישית היא סיסמת ההתקשרות ביניהם.

כתבו את הפונקציות הבאות:

  1. load_file – טוענת קובץ טבלאי שהשורה הראשונה שבו היא כותרת, והעמודות שבו מופרדת זו מזו בתו |.
    הפונקציה תחזיר רשימה של מילונים. כל מילון ברשימה ייצג שורה בקובץ. המפתחות של כל מילון יהיו שמות השדות מהכותרת.
  2. get_user – שמקבלת את שם המשתמש, ומחזירה את מספר המשתמש שלו.
  3. get_password – שמקבלת שני מספרים סידוריים של משתמשים ומחזירה את סיסמת ההתקשרות בינם.
  4. decrypt_file – שמקבלת מפתח ונתיב לקובץ, ומפענחת אותו באמצעות הפונקציה decrypt.

לצורך פתרון החידה, מצאו את סיסמת ההתקשרות של המשתמשים 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))