שבוע 9 – יום 4: קשטנים (Decorators)

קשטנים – Decorators

הקדמה

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

רענון

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

length = len
print(length("Hello"))

Output: 5

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

old_len = len
len = lambda x: 0
print(len("Hello"))

Output: 0

נסו, בשביל התרגיל, לשנות את len כך שהיא תחזיר את אורך ה־sequence שהיא קיבלה כפול 2.
עבור “Hello” הפונקציה תחזיר 10, ועבור “A” היא תחזיר 2.

פתרון
old_len = len
len = lambda seq: old_len(seq) * 2

פונקציות מקוננות

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

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

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

def create_logger(logger_name):
    def log(message):
        print(f"[{logger_name}] {message}")
    return log


log_error = create_logger("Errors")
log_system = create_logger("System")
log_error("CRITICAL FAILURE: Can't find Nemo.")
log_system("Chop Suey")

Output:
[Errors] CRITICAL FAILURE: Can’t find Nemo.
[System] Chop Suey

שימו לב שלפונקציה הפנימית שמוחזרת, log, יש גישה לכל המשתנים והפרמטרים שהוגדרו לפני יצירתה ב־create_logger.

תרגול ביניים: מח(שוב?)ון

כתבו פונקציה בשם calc שמקבלת כפרמטרים שני מספרים.

הפונקציה תחזיר מילון של פונקציות:

  • הערך של המפתח 'add' יהיה פונקציה שמבצעת חיבור בין שני המספרים.
  • הערך של המפתח 'sub' יהיה פונקציה שמבצעת חיסור בין שני המספרים.
  • הערך של המפתח 'mul' יהיה פונקציה שמבצעת כפל בין שני המספרים.
  • הערך של המפתח 'div' יהיה פונקציה שמבצעת חילוק בין שני המספרים.

השתמשו בפונקציות מקוננות.

עטיפת פונקציה

נשלב את הטכניקות שלמדנו עד כה.
נניח שאנחנו רוצים ליצור פונקציה בשם debug שמקבלת כפרמטר פונקציה אחרת (f), מדפיסה “Start” לפני ש־f רצה ו־“Stop” אחרי ש־f מסיימת את ריצתה.
נסו לממש כזו בעצמכם :slight_smile:

פתרון
def debug(f):
    print("Start")
    f()
    print("Stop")

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

פתרון
def debug(f, *args, **kwargs):
    print("Start")
    return_value = f(*args, **kwargs)
    print("Stop")
    return return_value

נחמד!
נסו להשתדרג אפילו יותר.
צרו פונקציה (debug) שמקבלת פונקציה (f), ומחזירה פונקציה חדשה, שיודעת לקבל את אותם ארגומנטים של פונקציה f המקורית (רמז: השתמשו ב־args, kwargs).
הפונקציה שתחזור מ־debug תפעל בדיוק כמו f ותחזיר את אותו ערך, רק שלפני הרצתה של f יודפס Start ואחרי הרצתה יודפס End.
זה רעיון מגניב במיוחד כי אנחנו בעצם יוצרים פונקציה שיודעת לשנות התנהגות של פונקציה קיימת.

פתרון
def debug(f):
    def wrapper_function(*args, **kwargs):
        print("Start")
        return_value = f(*args, **kwargs)
        print("Stop")
        return return_value
    return wrapper_function

דוגמה לריצה:

def write_file(filename, content):
    print("Running write_file...")
    with open(filename, "w") as f:
        f.write(content)


debug_write_file = debug(write_file)
debug_write_file("foo.txt", "Hello World")

תוצאה:

Start
Running write_file…
Stop

מטורף שזה עובד :scream:
עכשיו, טכנית, אנחנו יכולים פשוט לכתוב:

write_file = debug(write_file)
write_file("foo.txt", "Hello World")

ונראה שהפונקציה write_file שלנו מדפיסה כל פעם שהיא מתחילה ומסיימת!
נראה דוגמה נוספת:

def add(a, b):
    return a + b

add = debug(add)
add(5, 6)

ואילו על פונקציות שמגיעות עם פייתון:

len = debug(len)
len("Hello!")

מתי מגיעים למה זה Decorators כבר?

אה, אחרי שהבנתם את כל מה שכתוב למעלה אתם כבר די שם.
הרעיון של decorator זה לרשום את השורה add = debug(add) בצורה קצת יותר יפה.
במקום לדרוס את הפונקציה בשורה נפרדת שלא נראית קשורה לפונקציה, אפשר לעשות ככה:

@debug
def add(a, b):
    return a + b

מעל הפונקציה שמנו כרוכית, ואחריה את שם הפונקציה שאנחנו רוצים שתעטוף את add.
בפועל, מאחורי הקלעים, פייתון עשתה בשבילינו: add = debug(add). זה כל הסיפור.

תרגילים

תרגיל 1: יומנגוס

כתבו decorator בשם upper.
כשה־decorator יוחל על פונקציה כלשהי שמחזירה (f, לדוגמה), הוא ישנה את הפונקציה f כך שערכי ההחזרה שלה תמיד יוחזרו באותיות גדולות.
אתם יכולים להניח ש־f תמיד מחזירה מחרוזת.

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

כתבו decorator בשם profile.
כשה־decorator יוחל על פונקציה כלשהי (f, לדוגמה), הוא ישנה את הפונקציה f כך שבכל קריאה ל־f יכתב לקובץ log.txt מתי היא נקראה, וכמה זמן לקח להריץ אותה.

תרגיל 3: קיבוץ גלויות

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

נדמיין לדוגמה את הקבצים a.txt ו־b.txt:

a.txt
Hello
b.txt
Bye

ואת הפונקציה join_files:

def join(text, text2, reverse=False):
    if reverse:
        return text2 + text
    return text + text2

אם נקרא סתם כך ל־join_files, נראה שהיא בסך הכל משרשרת שתי מחרוזות זו לזו: join('Mushu', 'Cricket') יחזיר MushuCricket.
אם נחיל את read_files שתתכנתו על הפונקציה join, בצורה הבאה:

@read_files
def join(text, text2, reverse=False):
    if reverse:
        return text2 + text
    return text + text2

הפונקציה תמשיך לעבוד אותו דבר אם תקבל כפרמטרים מחרוזות רגילות, אך אם תקבל נתיב לקובץ, היא תקרא את תוכנו במקום: join('a.txt', 'b.txt') יחזיר “HelloBye”.

מקורות לקריאה

על החומר הנלמד

  1. על decorators ב־Real Python.
  2. מדריך קצרצר נוסף על הנושא.
  3. הסבר מעמיק ומתקדם יחסית.

להרחבה

  1. קראו על functools.wraps.
  2. קראו (או נסו בעצמכם) על איך משתמשים ב־class ליצירת decorators.
  3. קראו (או חשבו בעצמכם) על איך אפשר לגרום ל־decorator עצמו לקבל פרמטרים: @log(a, b), לדוגמה.
  4. קראו (או חשבו בעצמכם) על מה קורה אם רוצים להחיל יותר מ־decorator אחד על פונקציה ועל איך עושים זאת.
5 לייקים