דקורטורים בפייתון - צלילה לעומק ב-2 חלקים (2/2)
- ולריה איינבינדר
- תוכנה
- 17 Jul, 2024
זהו סדרה בת 2 חלקים:
- חלק 1: מבוא + יצירת הדקורטור הראשון שלך
- חלק 2 (נוכחי): דקורטורים לפונקציות שמקבלות פרמטרים ומחזירות ערכים, דקורטורים שמקבלים ארגומנטים, דקורטורים במחלקות + תרגילים מתקדמים עם פתרונות
מאמר זה הוא חלק 2 בסדרה על דקורטורים בפייתון, והוא מניח שיש לכם ידע בסיסי בדקורטורים. אם אין לכם, או אם לא קראתם את המאמר הראשון בסדרה, אתם מוזמנים לעשות זאת כאן.
במאמר זה נדון במימוש דקורטורים מתקדמים יותר, באופן ספציפי:
- מימוש דקורטורים לכל פונקציה, כולל פונקציות שמקבלות פרמטרים ומחזירות ערכים
- מימוש דקורטורים שמקבלים ארגומנטים בעצמם (כלומר, שינוי התנהגות הדקורטור)
- מימוש דקורטורים בתוך מחלקות
- תרגילים לשיפור היכולות שלכם + פתרונות
אם אתם מעדיפים לצפות ולהאזין, אתם מוזמנים לבדוק את הסרטון שלי שמסביר את הנושאים האלה:
בואו נתחיל עם דוגמה שמראה למה אנחנו צריכים דקורטורים.
חזרה על החומר
נתחיל בחזרה קצרה על מהו דקורטור ומה שלמדנו בחלק הקודם של הסדרה. להלן מימוש של greeting_decorator פשוט שמדפיס הודעות יפות לפני ואחרי הרצת הפונקציה שהוא מקשט.
מכיוון שהגדרנו את sum_of_digits עם הדקורטור greeting_decorator, הפונקציה האמיתית sum_of_digits שנשמרה על ידי מפרשן פייתון היא הפונקציה המוחזרת על ידי greeting_decorator. במילים אחרות, הקוד שנשמר עם המזהה sum_of_digits הוא הערך המוחזר של:
greeting_decorator(sum_of_digits)
כאשר אנו קוראים:
sum_of_digits()
מפרשן פייתון מבצע את קוד הדקורטור, שבתורו מציג הודעות ברוכים הבאים ולהתראות ומבצע את קוד הפונקציה המקורית sum_of_digits. לכן, הפלט עבור שורה זו יהיה כדלקמן:
ייתכן ששמתם לב שבחרתי לממש את sum_of_digits בצורה מעט מוזרה: במקום להעביר מספר כארגומנט ולהחזיר את סכום הספרות שלו כערך החזרה, בחרתי בכוונה לקבל מספר כקלט ולהדפיס את התוצאה בתוך הפונקציה. הדבר נעשה כדי לפשט את מימוש הדקורטור שלנו. אבל עכשיו, אחרי שיש לנו הבנה בסיסית של דקורטורים, נוכל להמשיך הלאה.
דקורטור לפונקציות שמקבלות פרמטרים ומחזירות ערכים
בואו נשנה את הפונקציה sum_of_digits למימוש סטנדרטי שמבצע יותר היגיון. עכשיו הפונקציה שלנו תקבל מספר כפרמטר ותחזיר את הסכום.
אם ננסה להריץ:
sum_of_digits(235)
כאשר greeting_decorator נשאר כפי שהוא, נקבל חריגה:
זה קורה מכיוון שהפונקציה greeting_func שמומשה בתוך greeting_decorator לא מקבלת ארגומנטים, וכפי שכבר ידוע לנו — מה שבאמת ייקרא כאשר נריץ sum_of_digits(235) הוא:
greeting_decorator(sum_of_digits)(235)
זה אומר שאנחנו צריכים לשנות את greeting_func שמוחזרת מ-greeting_decorator כך שתוכל לקבל ארגומנטים ולהחזיר ערך, כפי שמוצג להלן:
עכשיו, כאשר נקרא:
ret_sum = sum_of_digits(345)
print(f"Sum of digits for 345 is: {ret_sum}")
נקבל את הפלט הצפוי:
למרות ש-greeting_decorator שלנו כעת עובד כמצופה, הדרך שבה הוא מטפל בארגומנטים של פונקציות אינה כללית מספיק. מה יקרה אם ננסה להשתמש ב-greeting_decorator עבור פונקציה שמקבלת יותר מארגומנט אחד? או אם הפונקציה המקושטת תכלול גם ארגומנטים דרושים וגם מילות מפתח? או אם הפונקציה לא מקבלת ארגומנטים כלל? כמובן, זה שוב יגרום לחריגה עבור כל המקרים הללו, מכיוון שכעת אנחנו דורשים שהפונקציה המקושטת תקבל בדיוק ארגומנט אחד.
למזלנו, בפייתון, אנחנו יכולים לשכתב את הדקורטור שלנו כדי לתמוך בכל המקרים המתוארים לעיל ואף יותר, באמצעות הסימן *args ו-**kwargs:
עכשיו, אנחנו יכולים להשתמש ב-greeting_decorator עם כל פונקציה!
לסיכום: כשמממשים דקורטור, יש לעבוד לפי התבנית הבאה:
עכשיו זה הזמן לתרגיל. אני מעודדת אתכם לנסות לפתור אותו בעצמכם 💪 לפני שאתם מסתכלים על הפתרון שלי.
תרגיל 1: מימוש דקורטור שמנהל רישום זמן ביצוע של פונקציה
מממשו דקורטור performance_log שמדפיס את כמות הזמן (בשניות) שלוקח לפונקציה להשלים את הביצוע. זה יכול להיות מאוד שימושי לצורכי דיבוג ופרופילינג.
בדקו את הדקורטור שלכם עם הפונקציות המוצעות למטה.
רמז: נסו להשתמש ב-time.perf_counter() כדי למדוד זמן.
להלן דוגמאות לפלט שתקבלו כאשר תריצו את long_running_func לאחר שתממשו את הדקורטור:
res = long_running_func(17, 1000)
print(f"The result is: {res}")
פלט צפוי:
Execution time is: 0.00027704099989023234
The result is 281139182902740093173255193460516433570993900889613439277903794687196783510046951084......
פתרון אופציונלי לתרגיל זה ניתן למצוא כאן, אבל אני בטוחה שאין לכם צורך בו כי פתרתם את זה בעצמכם 😅
אזהרת ספוילר⚠️: החלק הבא מבוסס על הדקורטור performance_log.
העברת פרמטרים לדקורטורים
לאחר שמימשנו את הדקורטור performance_log שמנהל רישום זמן ביצוע בשניות, אנחנו רוצים להוסיף יותר גמישות ולאפשר לנו לבחור את יחידת הזמן שבה נציג את זמן הביצוע: שניות, מילי-שניות, או ננו-שניות. במילים אחרות, אנחנו רוצים לממש את הדקורטור performance_log כך שיאפשר לנו להעביר לו פרמטר, כמו אחד מהבאים: "s", "ms", או "ns", כך:
@performance_log(time_units="ms")
def some_function():
pass
איך אפשר להשיג זאת? נתחיל מהדקורטור המקורי performance_log:
אנחנו יודעים שכאשר מפרשן פייתון רואה את השורות הבאות:
@performance_log
def some_function():
pass
הוא מבצע את הפעולה הבאה:
some_function = performance_log(some_function)
והצפוי הוא שסוג ההחזרה של השורה performance_log(some_function) יהיה פונקציה שמקבלת פונקציה אחרת כפרמטר ומחזירה פונקציה מקושטת.
כעת, באופן דומה, אנחנו צריכים לעדכן את performance_log כך שיחזיר פונקציה (כמו הקודמת). ננסה לממש אחת:
עד כה, כל טוב. אנחנו רק צריכים להוסיף מימוש לפונקציה wrapper שמוחזרת. לפי הדרישות שהוזכרו קודם, הפונקציה הזו צריכה לקבל פונקציה כפרמטר ולהחזיר את הפונקציה המקושטת שלה. נבצע זאת:
אנחנו כמעט שם! נותר רק להשלים את מימוש הדקורטור האמיתי (הוא יהיה דומה מאוד למימוש המקורי, אך ייקח בחשבון את הפרמטר time_units שניתן).
כפי ששמתם לב, מימוש דקורטור שיכול לקבל פרמטרים חייב אותנו להוסיף עוד "שכבה" של הגדרת פונקציה, אך כל עוד אתם מבינים את התהליך שמאחורי הקלעים, לא תיתקלו בקשיים במימוש דברים כאלה.
מימוש דקורטורים בתוך מחלקה
עד כה, נהגנו לממש דקורטורים כפונקציות נפרדות. עם זאת, ניתן ואף מומלץ לממש אותם בתוך מחלקות, במיוחד אם דקורטור מסוים קשור ללוגיקה של הקוד של המחלקה.
בואו נסתכל על דוגמה. יש לנו את המחלקה Bank הבאה:
כפי שאתם רואים, המחלקה מממשת שלוש פונקציות עיקריות: withdraw(), deposit(), ו-feedback(). השתיים הראשונות הן קריטיות, ולכן אנחנו רוצים לוודא שהן לא ייקראו מחוץ לשעות העבודה של הבנק. הפונקציה feedback() אינה קריטית ולכן ניתן לקרוא לה בכל עת. אנחנו רוצים לממש דקורטור בשם working_hours_only שיקשט את הפונקציות הקריטיות שצריכות להיקרא רק במהלך שעות העבודה של הבנק. מכיוון שהדקורטור הזה קשור באופן הדוק ללוגיקה של המחלקה Bank, והוא למעשה חלק בלתי נפרד ממנה, זה הגיוני לממש את הדקורטור הזה בתוך המחלקה.
נקודה טכנית ❗️חשובה❗️ שיש לזכור כאשר מגדירים דקורטור בתוך מחלקה היא שחתימת הדקורטור לא צריכה לכלול את הפרמטר self, מכיוון שהוא לא מועבר לדקורטור על ידי מפרשן פייתון. ואם חושבים על זה, זה לגמרי הגיוני. למה? כי הגדרת פונקציות המחלקה, כולל "החלפת" הפונקציות המקוריות באלו המקושטות, מתבצעת במהלך שלב הגדרת המחלקה, כאשר אין עדיין מופע של המחלקה בתמונה. אפשר לחשוב על דקורטורים כעל פונקציות סטטיות המוגדרות בתוך מחלקה. לכן אנחנו לא מצפים ואין צורך שהפרמטר self יועבר שם.
לאחר שדנו במימוש דקורטורים בתוך מחלקה, אתם מוכנים לממש את הדקורטור working_hours_only.
תרגיל 2: מימוש דקורטור working_hours_only
בתוך מחלקת Bank
מימוש דקורטור working_hours_only שמוודא שפונקציה נקראת רק במהלך שעות העבודה (א'-ה', 09:00 - 17:00). לדוגמה, קריאה לפונקציה feedback() בשבת צריכה להגיע לפונקציה feedback וההודעה "Called feedback" צריכה להיות מודפסת. קריאה לפונקציה withdraw() בשבת לא צריכה להגיע לפונקציה withdraw האמיתית וצריכה לגרום לחריגה.
כאן תוכלו לבדוק את הפתרון שלי לתרגיל זה.
תרגיל 3: שינוי המימוש של הדקורטור working_hours_only
כך שיקבל ימי עבודה ושעות עבודה של הבנק כפרמטרים
אני לא מפרסמת פתרון לתרגיל זה, אך אני מעודדת אתכם לפתור אותו ולפרסם את הפתרונות שלכם בתגובות. אני אשמח לציין את הפתרון הטוב ביותר כאן כולל קרדיט למחבר 😄
אני מקווה שנהניתם מהמאמר הזה. המאמר הבא יפורסם בקרוב, אז הישארו מעודכנים והרשמו לערוץ שלי כדי לקבל התראה ברגע שהוא יוצא!
כל הקוד ב-Google Colab ניתן למצוא כאן.