یکبار برای همیشه Decorator

This is the real decorator in Python!
This is the real decorator in Python!

پیشگفتار

سلام به همه متفکرین و محققین پایتون!

اگر عمری باقی باشه قصد داریم یه چرخی توی مفاهیم بنیادی پایتون بزنیم، اولین جلسه هم با Decorator می‌خوایم شروع کنیم. این مفاهیم مهم‌ترین ایرادی که میشه بهشون گرفت این هست که در نگاه اول پیچیده بنظر می‌رسند. توی مجموعه یکبار برای همیشه سعی شده که این سختی‌ها تا جایی ممکنه ساده‌سازی بشن و با مثال‌های مختلف این مطالب، تعریف بشن.

مقدمه

اما decorator چیه؟ تعریف خیلی ساده اش اینه که decorator ها امکان توسعه و تغییر رفتار یک چیز قابلِ فراخوانی را به ما میده. توسعه یا تغییری مد نظر ما هست که باعث تغییر در اون چیزِ قابل فراخوانی نشه! یعنی فقط یک decorator به کد ما اضافه بشه. اما منظور ما از چیزی که قابلیتِ فراخوانی داره چیه؟ این چیز‌ها سه نوع هستند: توابع، متد‌ها و کلاس‌ها. به چه چیزی می‌شه گفت قابلیت فراخوانی داره؟ هر چیزی که اگر اون رو به تابع callable بدیم خروجی True برگردونه.

مثلاً تست کنید:

callable(print) # which means Can I call print function?

توابع و متد‌ها به صورت پیش‌فرض قابلیت فراخوانی دارند. یعنی اسمشون رو می‌نویسیم و بعدش که پرانتز می‌ذاریم به این معناست که اون‌ها رو صدا زدیم یا کالِشون کردیم. آیا کلاس هم قابلیت کال(call) کردن داره؟ بله! اگه زمان تعریف کلاس متد '__call__' رو برای اون کلاس تعریف کنیم دقیقا مشابه یک تابع امکان فراخونی پیدا می‌کنه.

خیلی دور نشیم از بحث اصلی اما آیا واقعا نیازه من برم بفهمم decorator چی‌هست؟ چند از کاربرد‌های اون رو نشون بده حداقل.

بسته به اینکه چه استفاده کننده‌ای از پایتون هستید، مثال‌ها میتونه مختلف باشه.ماژول atexit یک برنامه ساده‌اس که میگه وقتی از برنامه خارج شدی چیکار بکن، این ماژول رو به دو صورت قابل استفاده است، حالت اول که تابع خودمون رو تعریف کردیم بعد به atexit گفتیم هر موقع خواستی از برنامه خارج شدی قبلش این تابع رو اجرا کن.

حالت دوم که برای ما حائز اهمیتِ، با استفاده از decorator هست. این علامت «@» رو هرکجا توی پایتون دیدید، سریع decorator باید تو ذهنتون بیاد. یادتونه موقع تعریف گفتم decoratorها امکان توسعه و تغییر میدن. این یک مثال از توسعه هست. در حقیقت ما بدون تغییر تابع خودمون موفق شدیم اون تابع رو با استفاده از decorator به نحوی توسعه بدیم که موقع خروج تابع ما اجرا بشه.

پس میشه اینجوری خلاصه‌اش کرد، ما به یک decorator یک چیزی(تابع، متد و یا کلاس) رو میدیم و اون چیز رو decorator تغییر میده و تغییر یافتشو به ما بر میگردونه. سینتکس کلی‌ رو میشه اینجوری تعریف کرد.

@our_decorator
def our_function(args):
....

که برابر هست با کد پایین(تقریباً):

def our_function(args):
....
our_function = our_decorator(our_function)

خیلی از جاها شما ممکنه decoratorها رو دیده باشید. مثلا اگه به شی‌گرائی علاقه‌مند باشید [email protected] ، @classmethod، و یا [email protected] مثالهایی از این قبیل هستند. اگه توسعه دهنده سمت سرور هستید مثلا فریم‌ورک فلسک پر هست از decorator که بدون اونا زندگی چقدر سخت میشه! لیستی از decoratorهای معروف پایتون: لینک!

اما تعریف و تمجید از آقای decorator بسه، بریم سراغ اینکه چطور یه دونه بنویسیم و ازش استفاده کنیم. پیش از این باید چندتا از پیش‌نیاز‌های decorator که مفاهیم اولیه پایتون در رابطه با توابع هستند رو توضیح بدم.

توابع در پایتون

یک جمله‌ی خیلی مشهوری در پایتون وجود داره که میگه «توابع در حقیقت "First-Class" آبجکت هستند». یعنی توابع توی پایتون به تمامی حالت‌ها قابل استفاده هستند. برخلاف بقیه چیز‌ها که ممکنه اینجور نباشن. به همین خاطر بهشون میگیم First-Class (بهترینمون هستند). ویژگی‌های توابع عبارتند از:

  • توابع در پایتون مثل هر چیز دیگه یک آبجکت حساب میشن. در نتیجه می‌تونیم مثل یک آبجکت اونارو به متغییر انتصاب کنیم:

اگر از تابع id بپرسیم که این‌دوتا تابع به چه آدرسی از حافظه اشاره می‌کنن؟ جوابشون قابل تامله!

هر دوی اونا به خونه از حافظه اشاره می‌کنن و وقتی تابع اصلی رو پاک می‌کنیم هنوز توی اون آدرس حافظه تابع موجوده و تابع دوم به کارش ادامه میده.

  • تابع را می‌توان به تابع‌ِ دیگر به عنوان ورودی پاس داد.
  • توابع می‌توانند تودرتو باشند!

خب اگه از زبان‌هایی مثل سی و دوستاش به پایتون مهاجرت کرده باشید، شاید اولش این یکم عجیب باشه واستون، اما خب اینجا میشه تا تعدادِ زیادی تابع تودرتو تعریف کنید.

نکته اینکه قطعاً انتظار نداریم که به تابع درونی دسترسی داشته باشیم! مگر اینکه اون تابع داخلی مورد نظرمون رو return کنیم. چیزی شبیه پایین:

یه چیزه دیگه هم هست، تابع داخلی میتونه به ورودی‌های تابع بیرونی دسترسی داشته باشه! چطوره مثال بالا رو طوری تغییر بدیم که متن هم همونجا بگیره و به تابع درونی پاس بده؟!

  • آبجکت‌ها هم می‌توانند فراخوانی شوند!

اگه یادتون باشه،‌گفتیم توابع توی پایتون آبجکت هستند. اما آبجکت‌ها که تابع نیستند. یعنی هر آبجکت ذاتا قابل فراخوانی نیست. اما میشه کاری کرد که آبجکت‌ها نیز قابلیت فراخوانی پیدا کنند، فقط باید این کار فعال بشه. مثال:

با متد __call__ میشه این قابلیت رو به آبجکت مورد نظر داد.

خب الان تمام چیز‌هایی که نیازه رو یاد گرفتیم، بریم با خود decorator آشنا شیم.

معرفی Decorator

بریم اولین decorator رو تعریف کنیم، برای راحتی کار بریم یک decorator تعریف کنیم که هیچ‌کاری نمی‌کنه!

خُب یک تابع خیلی ساده به اسم say_hello تعریف کردیم و یک decorator که هیچ کاری نمی‌کنه جز اینکه تابعی که بهش پاس داده شده رو برگردونه. بعدش که تابع اولیه رو اجرا می‌کنیم می‌بینیم همون‌طور که انتظار داریم هیچ‌چیزی تغییر نکرده! اگه یادتون باشه اون بالا گفتیم نماد decorator، @ هست خب یکبار با این نماد از decorator استفاده کنیم:

پس تا اینجا یادگرفتیم که اولاً یک decorator خیلی پیش‌پا افتاده تعریف کنیم، ثانیاً یاد گرفتیم چطوری کاملا دستی (manually) decorator رو بکار ببریم و ثالثاً یک چیز رو اتوماتیک decorate کنیم.

اما بریم واقعا با decorator یک‌کاری انجام بدیم:

کاری که این‌سری انجام گرفته، این هست که ما یک decorator به اسم make_uppercase تعریف کردیم که یک تابع رو به عنوان ورودی می‌گیره و درون خوش یک تابع داره که خروجی اون تابع رو به عنوان خروجی خودش برمی‌گردونه(اصطلاحاً به توابع این‌شکلی closure(بخونید کلوژر) گفته میشه).

اعمال چندین decorator

مثال: فرض کنیم قرار هست، با استفاده از چندین decorator کاری کنیم که بتوانیم تگ‌های html را به رشته موردنظر اضافه کنیم:

که طبق مطالب قبلی میشه اینجوری هم تعریفش کرد:

نوشتن decoratorای که آرگومان می‌پذیرد!

تا این‌جای کار تابع‌هایی که نوشتیم خیلی ساده بودند، هیچ‌گونه پارامتر ورودی نداشتند (say_hello) و در نتیجه هیچ‌ مشکلی در پاس دادن پارامتر از تابع decorator (مثلا bold/italic) به تابع‌ای که قراره decorate بشه (say_hello) نبود.

اما اگر تابعی که نوشتیم ورودی می‌گیره دیگه با این حالت به مشکل می‌خوریم. باید به طریقی به تابع decorator بگیم که تمام پارامتر‌هایی که بدستت میرسه رو به تابع اصلی ما هم بده. اینجاس که این دو برادر args* و kwargs** به کمک ما میان (اگه نمی‌دونید اینا چی هستن از این لینک استفاده کنید!)، یک مثال با هم ببینیم:

یک مثال به عنوان جمع بندی کار

تا اینجای کار گفتیم که decorator چی هست و چه‌جوری تعریفش کنیم. اما بنظرم خوبه که بریم یک مثال خوب هم باهاش ببینیم:

فرض کنیم ما بی‌خبریم از اینکه ماژول‌هایی هست که به صورت خیلی دقیق به ما میگن هر تابع چقدر طول میکشه تا اجرا بشه و قصد داریم حالا که یاد گرفتیم decorator چی هست، بریم یکی تعریف کنیم با اسم timeit که بره این کار رو برای هر تابعی که خواستیم انجام بده. اول از همه تعریفش کنیم:

خب دقیقاً مشابه چیزی که تا الان یاد گرفتیم، تابع رو از ورودی گرفتیم پاس دادیم به تابع درونی wrapper هرچی ورودی داشتیم هم دادیم به اون تابع، منتهی این سری، دو متغییر هم اضافه کردیم که بفهمیم چقدر زمان صرف اجرای این تابع شده. بریم از اولین دستاوردمون استفاده کنیم( یادتون که ماژول time رو هم import کنید):

تابعی که تعریف کردیم از ورودی یک عدد میگیره و به اون تعداد ثانیه متوقف میشه، همون‌طور که پیداس ما بهش گفتیم سه ثانیه متوقف شو! خروجی decorator ِای که تعریف کردیم پاینش چاپ شده.

نکته‌ها:

  • با استفاده از decorator موفق شدیم در تابع ورودی تغییر یا توسعه‌ای بدیم که بتونیم بفهمیم چقدر زمان میبره تا این تابع اجرا بشه.
  • این کار بدون اینکه بریم تابع مورد نظر رو تغییر بدیم صورت گرفته، تصور کنید چقدر پس این decorator می‌تونه مفید باشه.

از اینجا کجا بریم

یک ماژولی که دارای decorator خیلی باحالی هست

https://stackoverflow.com/questions/308999/what-does-functools-wraps-do

https://mrcoles.com/blog/3-decorator-examples-and-awesome-python/

https://gist.github.com/Zearin/2f40b7b9cfc51132851a

کتاب Python Tricks صفحه ۷۴ خیلی توضیحات مفصل و کاملی در رابطه با decorator داده.

کلام آخر

امیدوارم که از این مطلب خوشتون اومده باشه، اگر این‌طور بوده حتما این مطلب با پایتون دوست‌های دیگه به اشتراک بذارید. تا یکبار برای همیشه‌ی دیگه خدانگهدار!