איך decorators עובדים (פייתון)



מה זה decorators?

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

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

הסינטקס פשוט מאוד ויראה כך:
@twice
def say_hello()
print('hello')
>> say_hello()
hello
hello

הדקורטורים בפייתון עובדים כמו ומממשים עיקרון דומה ל decorator design pattern (שבדרך כלל נראה בשימוש במחלקות)

מתי נצטרך decorators?

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

בואו נגיד והיינו רוצים להדפיס ללוג את שם הפונקציה ואת הפרמטרים שלה בכל פעם שהפונקציה נקראת.
יכולנו לעשות זאת כך:
def func(arg1, arg2):
logger.info(f'running func with {arg1} and {arg2}')
# do something with arg1 and arg2
view raw bad_logging.py hosted with ❤ by GitHub

ואותה שורת לוג הייתה נמצאת לנו בכל פונקציה

אבל דקורטרים נותנים לנו יכולת לגרום לאותה פונקציונאליות להיות הרבה יותר אלגנטית, פחות משוכפלת ולהראות כך:
@log
def func(arg1, arg2):
# do something with arg1 and arg2
view raw good_logging.py hosted with ❤ by GitHub

איך decorators עובדים בפייתון?

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

first class function

הקונספט של first class function מאפשר לנו להשתמש בפונקציות כ-first class citizen. כלומר, מאפשר לנו להעביר פונקציות כפרמטר לפונקציות אחרות, לעשות השמה של פונקציות לתוך משתנה, להחזיר אותם כערך מפונקציה ואפילו למיין אותם במערך.

לדוגמה,
def first_class():
print('im a first class func')
>> func_var = first_class
>> func_var()
'im a first class func'
אפשר לראות שביצענו כאן השמה של הפונקציה first_class לתוך func_var, ואז הרצנו את func_var ממש כפונקציה.

closures

הקונספט של כתיבת פונקציה בתוך פונקציה כך שהפונקציה הפנימית מכירה וזוכרת משתנים מתוך הפונקציה החיצונית גם לאחר ההרצה.
def print_msg():
msg = 'hello' # just a variable
def inner_print():
print(msg)
return inner_print
>> func = print_msg()
>> func()
hello
# or the same but with an argument
def print_msg(msg):
def inner_print():
print(msg)
return inner_print
>> func = print_msg('hello')
>> func()
hello
view raw closures.py hosted with ❤ by GitHub
בדוגמאות הקוד אפשר לראות שיש לנו פונקציה בשם inner_print.
הפונקציה inner_print מוגדרת בתוך הפונקציה print_msg ומשתמשת במשתנה msg מבלי לקבל אותו.

בדוגמה השניה אנחנו רואים את אותו הקונספט בדיוק פשוט עם פרמטר שמתקבל מבחוץ (שימו לב שהפרמטר עדיין לא מגיע ישירות לתוך inner_print).

דוגמה למימוש decorators בפייתון

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

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

def decorator(original_func): # the outer function that gets a function as parameter
def wrapper(): # inner function that uses the original function but wraps it
print('im running before') # work before running
original_func() # original function execution
print('im running after') # work after running
return wrapper
def do_stuff():
print('I do stuff')
# USAGE:
decorated = decorator(do_stuff)
decorated()

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

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

כלומר:
decorated = decorator(do_stuff)
decorated()

יהיה שקול לפשוט לשים ()decorator@ מעל הפונקציה do_stuff ואז פשוט לקרוא ל do_stuff באופן ישיר.
הדרך השניה כמובן אלגנטית בהרבה.

decorator עם פרמטרים

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

המימוש במקרה הזה יראה כך:

def decorator(original_func): # the outer function that gets a function as parameter
def wrapper(*args, **kwargs): # inner function that uses the original function but wraps it
print('im running before') # work before running
original_func(*args, **kwargs) # original function execution
print('im running after') # work after running
return wrapper # return edited function
@decorator
def do_stuff(stuff, more_stuff):
print(f'I do {stuff} and {more_stuff}')
#USAGE:
>> do_stuff('work', 'even more work')
im running before
I do work and even more work
im running after

במקרה הזה פשוט העברנו פרמטרים לפונקציה do_stuff, הפרמטרים התקבלו על ידי args ו- kwargs בפונקציית ה wrapper (הפנימית) והועברו לפונקציה.


דקורטור להדפסת לוגים

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

def log(original_func): # the outer function that gets a function as parameter
def wrapper(*args, **kwargs): # inner function that uses the original function but wraps it
print(f'Running function {original_func.__name__} with args {args} and kwargs {kwargs}') # print func name and its params
original_func(*args, **kwargs) # original function execution
print(f'{original_func.__name__} is Done') # printing that function is done
return wrapper # return edited function that logs
@log
def do_stuff(stuff, more_stuff):
print(f'I do {stuff} and {more_stuff}')
# USAGE:
>> do_stuff('work', 'even more work')
# OUTPUT:
Running function do_stuff with args ('work', 'even more work') and kwargs {}
I do work and even more work
do_stuff is Done

סיכום

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

נתראה בפוסט הבא!

תגובות

  1. אהבתי את הדוגמה האחרונה שלך, אבל חושב שפשוט מאוד מימשת את זה בצורה שמישהו חדש לא יבין/ יחשוב שככה כותבים logים בפייתון, דבר שעשוי להטעות ציבור רחב.
    הייתי מציע לתקן את הכמה שורות קוד האלו בתוך ה-decorator wrapper הפנימי ולשים את הכמה שורות הקבועות של המודיול logging:
    logging.Logger('Main')
    logging.Formatter("...")
    logging.basicConfig(....)
    logging.X("....")

    השבמחק
  2. מה ההבדל בין *args ל *kwargs?

    השבמחק

הוסף רשומת תגובה

פוסטים פופולריים מהבלוג הזה

קודמתי לדרגת סיניור במיקרוסופט - מה למדתי בדרך

מהם קבצי DLL ואיך להשתמש בהם?

Rust Builder Pattern