من به عنوان یک یادگیرنده این مقاله رو مینویسم. اگر اشتباهی توی این مطلب مشاهده کردید، به شدت خوشحال میشم به هر طریقی که دلتون خواست ( ترجیحا کامنت ) این خطا رو بهم گوشزد کنید.
این نکته رو هم بگم که تلاشی برای استفاده از کلمات فارسی نخواهم کرد. ( مثل آرایه، تابع، شئ و ... ) صرفا چیزی رو استفاده میکنم که برای خودم جا افتاده باشه.
خب در واقع ما دکوریتور ها رو مینویسم تا فانکشن های دیگه رو بدون اینکه به کدشون دست بزنیم، ویرایش کنیم.
برای مثال، فرض کنید که ما توی کدمون تعداد زیادی فانکشن داریم، و تصمیم میگیریم که فانکشن هامون بعد از اجرا شدن، یک لاگ هم بندازن. میتونیم عمل لاگ انداختن رو توی دونه دونه فانکشن هامون پیاده سازی کنیم، یا اینکه یه دکوریتور بنویسم که این کارو واسمون انجام بده و در انتها فانکشن هایی رو که میخوایم بهش پاس بدیم. خب به نظر میاد استفاده از دکوریتور ها عاقلانه تر و راحت تر باشه!
پس تا الان یه نمای کلی از دکوریتور ها و کاربردشون داریم. حتی خوده اسم دکوریتور هم میتونه توی درک کاربردشون بهمون کمک کنه، دکوریتور یعنی تزئین کننده. چیزی که بدون تغییر در ماهیت و اصل اون فانکشن، میتونه روش تاثیر بزاره.
حالا برای نوشتن اولین دکوریتور، اول نیاز داریم با چند تا مفهوم آشنا بشیم.
First class functions
میدونیم که همه چیز توی پایتون آبجکته. ( شئ ) این مسئله راجب فانکشن هامون هم صدق میکنه. بله، فانکشن هایی که توی پایتون مینویسیم هم در نهایت تبدیل میشن به یک آبجکت. خب نکته ی این مسئله اینجاست که ما میتونیم با فانکشن هامون مثل یک آبجکت رفتار کنیم، میتونیم اونارو توی یه متغیر ذخیره کنیم، به عنوان یه آرگومان پاسشون بدیم به بقیه فانکشن هامون یه به عنوان یک آیتم توی یه لیست نگهش داریم. به فانکشن هایی که میشه باهاشون مثل یک آبجکت رفتار کرد، میگن first class functions.
Higher order functions
مختصر و مفید: فانکشن هایی رو میگن که میتونن بقیه فانکشن هارو به عنوان آرگومان دریافت کنن. یعنی میتونیم موقع کال کردنشون، بهشون فانکشن پاس بدیم.
Closures
خب این یکی یه مقدار سخت تره! توی زبان برنامه نویسی پایتون، ما میتونیم توی بدنه یک فانکشن، فانکشن دیگه ای رو تعریف کنیم. به فانکشنی که درون یک فانکشن دیگه تعریف میشه، میگیم nested function. این فانکشن، متغیر های لوکالِ فانکشنِ والد خودش رو میتونه نگه داره و ذخیره شون کنه. شاید درکش با کد خوندن راحت تر باشه!
def tag_maker(tag):
def echo_using_tag(text):
return f'<{tag}>{text}</{tag}>'
return echo_using_tag
توی این کد، ما دوتا فانکشن داریم، فانکشن اول یه تگ html رو به عنوان آرگومان میگیره. چیزی که بر میگردونه هم یک فانکشن هست، همونطور که قبلا توضیح دادم، echo_using_tag یک nested function محسوب میشه. با دقت این فانکشن رو بخونید، توی مقداری که این فانکشن بر میگردونه، از متغیر tag هم استفاده میشه. این متغیر مربوط میشه به فانکشن tag_maker هست ولی ما اینجا ازش استفاده کردیم. وقتی این فانکشن رو بر میگردونیم، مقداری که توی متغیر tag بوده رو به یاد خودش نگه میداره. به کد زیر نگاه کنید.
h1_tag = tag_maker('h1') h1_tag('this is a line of text') # >> '<h1>this is a line of text</h1>'
همونطوری که مشاهده میکنید، فانکشن برگشتی از tag_maker رو توی متغیر h1_tag ذخیره کردیم، پس الان میتونیم با متغیر h1_tag مثل یک فانکشن رفتار کنیم و کالش کنیم. ( چرا؟ چون مقدار برگشتی از tag_maker یک فانکشن هست و ما اون فانکشن رو توی این متغیر ذخیره کردیم ) و در نهایت وقتی این متغیرِ حاویِ فانکشن رو کال میکنیم، میبینیم مقداری که به tag_maker پاس داده بودیم ( h1 )، توی مقدار برگشتی این فانکشن دیده میشه. پس echo_using_tag متغیر tag که مربوط به tag_maker هست رو توی خودش ذخیره کرده و اون رو به یاد داره. به فانکشنی که این کار رو میکنه، میگیم closure.
پی نوشت: الان که دارم نگاه میکنم، درک این مفهوم بوسیله خوندن کد خیلی راحت تر از توضیح هایی هست که من دادم ((:
بریم سر اصل مطلب
حالا که با این مفاهیم آشنا شدیم، میتونیم به راحتی یک دکوریتور بنویسیم، به صورت کلی یک دکوریتور، توی دو لایه تعریف میشه: یک فانکشن و یک closure که داخل فانکشنمون تعریفش میکنیم.
لایه اول، فانکشنی که میخوایم تزئینش کنیم رو به عنوان یک آرگومان میگیره، با اینکار به فانکشنمون دسترسی داریم و میتونیم اجراش کنیم. سپس، هر رفتاری که دلمون بخواد رو توی لایه ی دوم پیاده سازی میکنیم. برای مثال اگه دکوریتورمون رو به منظور لاگ انداختن داریم مینویسیم، توی لایه دوم، فانکشن رو کال میکنیم، بعدش عمل لاگ انداختن رو انجام میدیم و در صورت لزوم مقدار برگشتی فانکشن اصلی رو بر میگردونیم. و در نهایت closure ای که نوشتیم رو از لایه اول بر میگردونیم تا با فانکشن اصلی جاشون رو عوض کنند. برای درک بهتر کد زیر رو بخونید.
def first_layer(func):
def second_layer(*args, **kwargs): # apply your wanted behavior
result = func(args, kwargs)
# apply your wanted behavior
return result
return second_layer
حالا برای استفاده از این دکوریتور از سینتکس زیر استفاده میکنیم.
@first_layer
def echo(text):
return text
اتفاقی که اینجا میافته، اینکه که first_layer فانکشن echo رو به عنوان یه آرگومان بر میداره و اونو جایگزین میکنه با مقدار برگشتی خودش. ( second_layer )
به یاد داشته باشید که اگه نمیخواید فانکشن اصلی رو جایگزین کنید، میتونید این کار رو انجام بدید:
decorated_echo = first_layer(echo)
دکوریتور هایی که خودشون هم آرگومان میگیرند
اگه با فریمورک فلسک کار کرده باشید، به دکوریتور هایی بر میخورید که علاوه بره فانکشن ورودی، آرگومان های دیگه ای هم میگیرند، به کد زیر توجه کنید:
@app.route('/endpoint/')
def my_view():
...
برای پیاده سازی این نوع فانکشن، نیاز دارید که دکوریتورتون رو توی سه لایه تعریف کنید. لایه اول، آرگومان مورد نظر رو میگیره و لایه دوم رو بر میگردونه، لایه دوم هم فانکشن رو میگیره و اون رو با لایه سوم جایگزین میکنه. برای اینکه یک تصور کلی از این نوع دکوریتور پیدا کنید، کد زیر رو بخونید.
def first_layer(argument):
def second_layer(func):
def third_layer(*args, **kwargs):
# apply your wanted behavior
result = func(args, kwargs)
# apply your wanted behavior
return result
return third_layer
return second_layer
چرا closure همیشه *args و **kwargs رو به عنوان آرگومان میگیره؟!
جواب ساده ای داره، موقع تعریف کردن دکوریتور، ما نمیدونیم فانکشن مورد نظر چه تعداد آرگومان و با چه اسم هایی میگیره، بنابراین با args و kwargs همه ی حالت هارو پوشش میدیم. و سپس از همونا استفاده میکنیم تا فانکشن اصلی رو صدا بزنیم.
توجه
این یک best practice هست که قبل از تعریف لایه آخر توی دکوریتورتون، از دکوریتور functools.wraps استفاده کنید.
منابع
Programming Terms: First-Class Functions by Corey Schafer
Programming Terms: Closures - How to Use Them and Why They Are Useful by Corey Schafer
کتاب ترفند های پایتون، دن بیدر، ترجمه محمد بابازاده و معین باباپور، انتشارات پژوهندگان راه دانش