איך decorators עובדים (פייתון)
דקורטורים הם קונספט פשוט ועוצמתי שקיים כמעט בכל שפת high level, נמצא בשימוש נרחב כמעט בכל פרויקט ומאפשר למשתמש להוסיף פונקציונאליות לפעולות מבלי להתערב במימוש הפעולה.
יכולה להיות לי הפונקציה הבאה say_hello שמדפיסה את המחרוזת hello
ואני יכול לכתוב דקורטור בשם twice לצורך העניין, שכשאשים אותו מעל פונקציה היא תבוצע פעמיים.
במקרה כזה כשאקרא ל say_hello, הפונקציה תרוץ פעמיים ותדפיס פעמיים את המחרוזת hello.
הסינטקס פשוט מאוד ויראה כך:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@twice | |
def say_hello() | |
print('hello') | |
>> say_hello() | |
hello | |
hello |
הדקורטורים בפייתון עובדים כמו ומממשים עיקרון דומה ל decorator design pattern (שבדרך כלל נראה בשימוש במחלקות)
מתי נצטרך decorators?
ישנם מקרים רבים בהם נרצה להשתמש בדקורטורים, בעצם, זו דרך אלגנטית מאוד להוסיף פונקציונאליות לפני ו/או אחרי פעולה מסויימת.
בואו נגיד והיינו רוצים להדפיס ללוג את שם הפונקציה ואת הפרמטרים שלה בכל פעם שהפונקציה נקראת.
יכולנו לעשות זאת כך:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def func(arg1, arg2): | |
logger.info(f'running func with {arg1} and {arg2}') | |
# do something with arg1 and arg2 |
ואותה שורת לוג הייתה נמצאת לנו בכל פונקציה
אבל דקורטרים נותנים לנו יכולת לגרום לאותה פונקציונאליות להיות הרבה יותר אלגנטית, פחות משוכפלת ולהראות כך:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@log | |
def func(arg1, arg2): | |
# do something with arg1 and arg2 |
איך decorators עובדים בפייתון?
דקורטרים מתבססים על שני קונספטים חשובים במדעי המחשב, שחשוב להבין על מנת שנבין איך הם עובדים.
first class function
הקונספט של first class function מאפשר לנו להשתמש בפונקציות כ-first class citizen. כלומר, מאפשר לנו להעביר פונקציות כפרמטר לפונקציות אחרות, לעשות השמה של פונקציות לתוך משתנה, להחזיר אותם כערך מפונקציה ואפילו למיין אותם במערך.
לדוגמה,
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
הקונספט של כתיבת פונקציה בתוך פונקציה כך שהפונקציה הפנימית מכירה וזוכרת משתנים מתוך הפונקציה החיצונית גם לאחר ההרצה.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
בדוגמאות הקוד אפשר לראות שיש לנו פונקציה בשם inner_print.
הפונקציה inner_print מוגדרת בתוך הפונקציה print_msg ומשתמשת במשתנה msg מבלי לקבל אותו.
בדוגמה השניה אנחנו רואים את אותו הקונספט בדיוק פשוט עם פרמטר שמתקבל מבחוץ (שימו לב שהפרמטר עדיין לא מגיע ישירות לתוך inner_print).
בדוגמה השניה אנחנו רואים את אותו הקונספט בדיוק פשוט עם פרמטר שמתקבל מבחוץ (שימו לב שהפרמטר עדיין לא מגיע ישירות לתוך inner_print).
דוגמה למימוש decorators בפייתון
אז עכשיו, אחרי שסיימנו עם כל ההקדמות נראה איך בפועל אנחנו משתמשים בקונספטים עליהם דיברנו וכותבים decorator פשוט. תוך כדי הכתיבה אסביר בדיוק את השלבים ואיך בעצם הכל מתחבר לכדי דקורטור.
דקורטורים בעצם מתבססים מאוד על מה שראינו עד עכשיו. דקורטור הוא פשוט פונקציה שמקבלת פונקציה אחרת ומחזירה פונקציה (שׁלישית) שעוטפת את הפונקציה שהתקבלה ומוסיפה לה פונקציונאליות.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 שמאפשרים לנו שליחת פרמטרים גמישה ודינאמית אל הפונקציה אותה אנחנו רוצים להפעיל.
המימוש במקרה הזה יראה כך:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 באופן הבא:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 (שמגביל את הרצת הפעולה אותה הוא עוטף בזמן) ועוד ועוד..
נתראה בפוסט הבא!
אהבתי את הדוגמה האחרונה שלך, אבל חושב שפשוט מאוד מימשת את זה בצורה שמישהו חדש לא יבין/ יחשוב שככה כותבים logים בפייתון, דבר שעשוי להטעות ציבור רחב.
השבמחקהייתי מציע לתקן את הכמה שורות קוד האלו בתוך ה-decorator wrapper הפנימי ולשים את הכמה שורות הקבועות של המודיול logging:
logging.Logger('Main')
logging.Formatter("...")
logging.basicConfig(....)
logging.X("....")
מה ההבדל בין *args ל *kwargs?
השבמחק