Menu

Decorators ב-Python: @functions, ארגומנטים ו-functools.wraps

מה הם בעצם decorators ב-Python, איך כותבים אחד משלכם, והדפוסים (ארגומנטים, שרשור, wraps) שהופכים אותם לשווים את השימוש.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

decorator הוא פונקציה שעוטפת פונקציה

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

הדוגמה הקצרה ביותר שאפשר:

shout הוא ה-decorator. הוא מקבל פונקציה (greet), בונה פונקציה חדשה (wrapper) שקוראת למקורית והופכת את התוצאה לאותיות גדולות, ומחזיר אותה. ההצבה מחדש greet = shout(greet) מחליפה את המקורית בגרסה העטופה.

דפוס ההצבה מחדש הזה נפוץ כל כך ש-Python נתנה לו תחביר ייעודי.

ה-@ הוא קיצור להצבה מחדש

@name בשורה שמעל def שקול ל-name = name(...) מיד אחרי הגדרת הפונקציה:

@shout נקרא "הפעילו את ה-decorator shout על הפונקציה הזו". Python מריצה greet = shout(greet) מיד אחרי ה-def: אותו מנגנון כמו קודם, פחות הקלדה.

בכל פעם שרואים @name, החליפו אותו בראש ב-function = name(function). זו כל המשמעות של התחביר.

טיפול בארגומנטים

רוב הפונקציות מקבלות ארגומנטים. decorator שימושי מעביר אותם הלאה. הביטוי המקובל הוא *args, **kwargs, הדרך של Python לקבל כל ארגומנט שהוא, כי לפונקציה העוטפת לא אמור להיות אכפת למה הפונקציה העטופה מצפה:

*args תופס את כל הארגומנטים הפוזיציונליים. **kwargs תופס את כל הארגומנטים בעלי השם. הפונקציה העוטפת מעבירה הכל לפונקציה העטופה בלי שינוי, ואז עושה את העבודה הנוספת שה-decorator נועד לה: כאן, הפיכת התוצאה לאותיות גדולות.

זו הצורה שרוב ה-decorators האמיתיים לובשים.

דוגמה שימושית יותר: מדידת זמן

הדפסה של כמה זמן לוקחת פונקציה:

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

שמירה על הזהות המקורית: functools.wraps

עיטור של פונקציה מחליף אותה, ולכן הפונקציה העטופה מאבדת את התכונות המקוריות __name__ ו-__doc__ שלה:

greet.__name__ הוא עכשיו "wrapper", וה-docstring נעלם. זה שובר את help(), את ה-tracebacks, וכל כלי שבודק את הפונקציה.

התיקון הוא שורה אחת: @functools.wraps(func) על הפונקציה הפנימית מעתיק את המטא-דאטה.

הוסיפו תמיד @wraps(func) לפונקציה הפנימית. זה לא עולה כלום וחוסך סשנים מפתיעים של דיבוג בהמשך.

Decorators עם ארגומנטים

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

שלוש שכבות נשמע הרבה. קראו מבחוץ פנימה:

  1. repeat(times=3) היא קריאה לפונקציה. היא מחזירה את decorator.
  2. decorator הוא ה-decorator האמיתי: הוא מקבל פונקציה ומחזיר פונקציה עטופה.
  3. wrapper היא הפונקציה העטופה שרצה בזמן הקריאה.

הצורה הזו מפעילה את @retry(times=5), @cache(maxsize=100), ו-decorators של פריימוורקים כמו @app.route("/users"). ברגע שמזהים את דפוס שלוש השכבות, כל המשפחה נקראת באותה דרך.

שרשור decorators

אפשר להפעיל יותר מ-decorator אחד על פונקציה. הם נערמים מלמטה למעלה: זה שהכי קרוב ל-def רץ ראשון:

add_exclaim עוטף ראשון ומוסיף את ה-!. אחר כך shout עוטף את זה והופך הכל לאותיות גדולות. הפלט הוא HI, ROSA!.

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

Decorators מובנים שתראו

Python והספרייה הסטנדרטית שלה מגיעות עם כמה decorators שתפגשו בקוד אמיתי:

  • @property הופך מתודה לתכונה מחושבת.
  • @staticmethod מסמן מתודה שלא משתמשת ב-self או ב-cls.
  • @classmethod מקבל את המחלקה בתור cls במקום מופע: מצוין לבנאים חלופיים.
  • @functools.lru_cache שומר תוצאות בזיכרון (memoization), כך שקריאות חוזרות עם אותם ארגומנטים מגיעות למטמון.

decorators של פריימוורקים (@app.route, @pytest.fixture, @dataclass) עובדים באותו מנגנון. שום דבר מיוחד: רק פונקציות שעוטפות פונקציות.

מתי לכתוב אחד, ומתי לא

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

וותרו על decorator כש:

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

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

הבא בתור: Type Hints

decorators הם מקום נפוץ לפגוש type hints בשטח: פונקציות ה-wrapper מוסיפות הרבה פעמים הערות טיפוס לחתימות שלהן. type hints הם פיצ'ר קטן שמשתלם מהר, והם מגיעים בהמשך.

שאלות נפוצות

מה זה decorator ב-Python?

decorator הוא פונקציה שמקבלת פונקציה אחרת ומחזירה פונקציה חדשה, בדרך כלל כזו שעוטפת את המקורית בהתנהגות נוספת. מפעילים אותו עם @decorator_name בשורה שמעל def. התחביר @ הוא קיצור של func = decorator_name(func).

למה משתמשים ב-decorators ב-Python?

כדי להוסיף התנהגות סביב פונקציה בלי לערוך את הגוף שלה: לוגים, מדידת זמן, מטמון, בדיקות הרשאה, אימות קלט, ניסיונות חוזרים. פריימוורקים משתמשים בהם הרבה: @app.route(...) ב-Flask, @pytest.fixture ב-pytest, ו-@property ו-@staticmethod המובנים.

אפשר לכתוב decorator משלי?

כן. decorator הוא פשוט פונקציה שמקבלת פונקציה ומחזירה פונקציה. רוב ה-decorators המותאמים אישית עוטפים את הקריאה המקורית בפונקציה פנימית קטנה שעושה משהו לפני הקריאה, אחריה או סביבה. השתמשו ב-functools.wraps על הפונקציה הפנימית כדי לשמור על השם וה-docstring של הפונקציה המקורית.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל