فهرست مطالب
تصور کنید ۳۰ تابع با منطق های پیچیده کسب و کار در برنامه خود نوشته اید.
در یک صبح شنبه بارانی، رئیس کنار میز شما میآید و میگوید:
“صبح شنبه بخیر! آن گزارشهای TPS را به خاطر داری؟ نیاز دارم تا داده های ورودی و خروجی در هر مرحله تولید گزارش را به صورت کامل ثبت کنید. برای اهداف حسابرسی با شرکت XYZ این اطلاعات باید آماده شوند. به آنها گفتم که میتوانیم این کار را تا روز دوشنبه ارسال کنیم.”
این درخواست فشار خون شما را افزایش خواهد داد یا باعث آرامش شما خواهد شد. بستگی به این دارد که آیا شما از دکوریتورهای پایتون به درک عمیقی دست یافتهاید یا خیر. بدون دکوریتورها، ممکن است سه روز آینده را صرف اصلاح هر یک از این 30 تابع کنید و با فراخوانی های دستی ثبت وقایع و گزارشگیری آنها را درهم بریزید. لحظات سرگرم کننده ای است، اینطور نیست؟ با این حال، اگر دکوریتورهای پایتون را بشناسید، با آرامش به رئیس خود لبخند میزنید و میگویید:
“کاووس جان نگران نباش، امروز تا ساعت ۲ بعد از ظهر این کار را انجام خواهم داد.”
درست پس از آن، كدی را برای دکوریتور عمومی audit_log@ تایپ میكنید (طول این کد تقریبا 10 خط است) و به سرعت در جلوی تعریف تابع قرار میدهید. بعد کد خود را ارسال میکنید و یک فنجان قهوه دیگر میخورید. لذت بخش است، نه؟
شاید در اینجا کمی بزرگنمایی میکنم. دکوریتورها میتوانند قدرتمند و اعجاب انگیز باشند. در این مطلب تا جایی پیش میرویم که بگوییم درک دکوریتورهای پایتون، نقطه عطفی برای هر برنامهنویس سختکوش پایتون است. درک صحیح دکوریتورها نیاز به درک کاملی از چندین مفهوم پیشرفته در زبان پایتون، از جمله ویژگی توابع کلاس اول (First Class Functions) دارند.
معتقدم که نتایج مثبت درک عمیق نحوه عملکرد دکوریتورها در پایتون، میتواند بسیار زیاد باشد.
قدرت Decorator پایتون
Decorator پایتون به شما امکان گسترش و اصلاح رفتارهای قابل فراخوانی (مانند متدها و کلاس ها) بدون اصلاح دائمی پارامتر قابل فراخوانی را میدهد. هر فعالیتی که به اندازه کافی عمومی باشد و بتوانید آن را به کلاس یا رفتارهای موجود اضافه کنید، یک مورد عالی برای استفاده در دکوریتور محسوب میشود. کلمه انگلیسی Decorator معادل فارسی تزئین است و در این مطلب گاهی از کلمه تزئین به جای دکوریتور استفاده خواهیم کرد.
به زبان ساده دکوریتورهای پایتون امکان اضافه کردن رفتار جدید به اشیا را میدهند. این رفتارهای جدید در قالب بسته بندیهای جدید شی خواهند بود.
کارهای تکراری که با استفاده از Decorator پایتون میتوانید انجام دهید شامل موارد زیر است:
- ثبت وقایع و گزارشگیری
- اجرای کنترل دسترسی و احراز هویت
- تعیین زمان اجرای توابع
- محدودیت های پراستفاده
- Caching (دخیره سازی موقت اطلاعات) و …
حال، چرا باید به کاربرد دکوریتورها در پایتون تسلط داشته باشید؟ بدون درک این کاربرد چیزهایی که در ابتدا گفتیم کاملا انتزاعی به نظر میرسد و فهمیدن این موضوع که دکوریتورها چقدر میتوانند در کار روزانه یک توسعه دهنده پایتون سودمند باشند، دشوار خواهد بود.
قطعا، فکر کردن برای اولین بار در مورد دکوریتورها کمی دشوار است، اما ویژگی بسیار سودمندی است که اغلب در فریمورک های شخص ثالث و در کتابخانه های استاندارد پایتون با آن مواجه خواهید شد. در این مطلب نهایت تلاشم را خواهم کرد تا بصورت گام به گام شما را با دکوریتور ها آشنا کنم و تعدادی از ویژگی های اعجاب انگیز دکوریتورها را ببینیم.
قبل از بررسی این موضوع، اکنون لحظه ای عالی برای یادآوری در مورد ویژگی های توابع کلاس اول (یا توابع به عنوان شهروندان درجه اول) در پایتون است. مهمترین نکات اساسی “توابع کلاس اول” برای درک دکوریتورها عبارتند از:
• توابع اشیاء هستند. توابع را میتوان درون متغیرها ذخیره کرد، یا توابع را به عنوان ورودی و خروجی به توابع دیگر داد. همه چیز در زبان برنامه نویسی پایتون از جنس شی است.
• توابع را میتوان در داخل توابع دیگر قرار داد. تابع فرزند میتواند از وضعیت محلی تابع والد استفاده کند. (lexical closures)
خب، برای انجام این کار آماده هستید؟ پس شروع کنیم.
مبانی Decorator های پایتون
حالا، دکوریتورها واقعا چه چیزی هستند؟ دکوریتورها تابع را “تزئین” یا “بسته بندی” میکنند و به شما اجازه میدهند تا قبل و بعد از اجرای تابع بسته بندی شده، کد دلخواهی را اجرا کنید.
Decorator پایتون به شما امکان تعریف بلوک های قابل استفاده مجدد را میدهد تا بتوانید رفتار سایر توابع را تغییر یا گسترش دهید. رفتار تابع فقط هنگام تزئین آن تغییر میکند.
پیاده سازی دکوریتور ساده چگونه است؟ به بیان ساده، Decorator پایتون یک تابع قابل فراخوانی است که یک تابع قابل فراخوانی دیگری را به عنوان ورودی دریافت میکند و میتواند آن را به عنوان خروجی برگرداند.
تابع زیر دارای این ویژگی است و میتواند سادهترین دکوریتوری باشد که میتوانید به صورت زیر بنویسید:
def null_decorator(func): return func
همانطور که مشاهده میکنید null_decorator یک تابع قابل فراخوانی است، و مقدار قابل فراخوانی func که یک تابع است را به عنوان ورودی دریافت میکند و آن تابع را بدون ایجاد تغییری در خروجی بر میگرداند.
اجازه دهید از آن برای تزئین (بستهبندی یا Decorate) تابع دیگری استفاده کنیم.
def greet(): return 'Hello!' greet = null_decorator(greet) >>> greet() 'Hello!'
در این مثال، تابع greet را تعریف کرده ام و بعد بلافاصله آن را از طریق تابع null_decorator تزئین کرده ام. میدانم که این موضوع چندان مفید به نظر نمیرسد. منظورم این است که دکوریتور تهی را به گونهای خاص طراحی کردهایم تا بیفایده باشد، درست است؟ این مثال نحوه کار سینتکس Decorator پایتون برای حالت ویژه را توضیح خواهد داد.
به جای فراخوانی صریح null_decorator روی greet و جایگزینی شی greet، میتوانید از decorator@ پایتون برای تزئین مناسب تر و زیباتر تابع استفاده کنید.
@null_decorator def greet(): return 'Hello!' >>> greet() 'Hello!'
قرار دادن خط null_decorator@ در مقابل تعریف تابع، همان چیزی است که به عنوان اولین تعریف تابع ارائه کردیم و بعد از طریق دکوریتور تابع را اجرا کردیم. استفاده از decorator@ فقط فرم دگرگون یافته و میانبری برای الگوی پرکاربرد Decorator پایتون است.
توجه داشته باشید که استفاده از decorator@ تابع را بلافاصله در زمان تعریف تزئین میکند. این سبب دسترسی دشوار به نسخه اصلی تزئین نشده میشود. بنابراین ممکن است برای حفظ امکان فراخوانی تابع تزئین نشده، گزینه های مختلفی را برای تزئین دستی انتخاب کنید (در ادامه در رابطه با این ویژگی توضیحات بیشتری خواهیم داد).
Decorator ها رفتار توابع را اصلاح میکنند
حالا که کمی بیشتر با سینتکس دکوریتور آشنا شدید، اجازه دهید دکوریتور دیگری بنویسیم که در واقع کاری را انجام میدهد و رفتار تابع تزئین شده را اصلاح میکند. در اینجا دکوریتور کمی پیچیدهتری وجود دارد که نتیجه تابع تزئین شده را به حروف بزرگ تبدیل میکند.
def uppercase(func): def wrapper(): original_result = func() modified_result = original_result.upper() return modified_result return wrapper
این دکوریتور بجای برگرداندن ورودی تابع مانند دکوریتور تهی، تابع جدیدی را on-the-fly تعریف میکند و از آن برای بستهبندی تابع ورودی استفاده میکند تا تابع، رفتار خود را در زمان فراخوانی تغییر دهد. تابع wrapper به تابع ورودی تزئین نشده دسترسی دارد و میتواند قطعه کدی را قبل و بعد از فراخوانی تابع ورودی اجرا کند. (به لحاظ فنی، اصلا نیازی به فراخوانی تابع ورودی ندارد)
به این توجه داشته باشید که چرا تابع تزئین شده اجرا نشده است. در واقع، فراخوانی تابع ورودی در این مرحله منطقی نخواهد بود. میخواهیم وقتی که دکوریتور در نهایت فراخوانی میشود، بتواند رفتار تابع ورودی خود را اصلاح کند.
ممکن است بخواهید یکی دو دقیقه در مورد آن فکر کنید. میدانم که این مسئله چقدر میتواند دشوار به نظر برسد، اما قول میدهم که با کمک یکدیگر این مسئله را حل خواهیم کرد. وقت آن رسیده که در عمل Decorator پایتون که حروف را به حروف بزرگ تبدیل میکند را ببینیم. اگر با استفاده از آن تابع اصلی greet را تزئین کنید، چه چیزی رخ خواهد داد؟
@uppercase def greet(): return 'Hello!' >>> greet() 'HELLO!'
امیدوارم این همان نتیجه ای که انتظار آن را داشتید، باشد. اجازه دهید نگاهی دقیق تر به آنچه که در اینجا اتفاق افتاده، بیاندازیم.
برخلاف null_decorator، دکوریتور upercase@ هنگامی که تابع را تزئین میکند، شیء متفاوتی از تابع را بر میگرداند.
>>> greet <function greet at 0x10e9f0950> >>> null_decorator(greet) <function greet at 0x10e9f0950> >>> uppercase(greet) <function uppercase.<locals>.wrapper at 0x76da02f28>
در اینجا نکته ی کوچکی وجود دارد، Decorator پایتون باید این کار را انجام دهد تا در هنگام فراخوانی نهایی، رفتار تابع تزئین شده را اصلاح کند. دکوریتور upercase@ خود نیز تابع است. تنها روش برای اثرگذاری بر “رفتار آتی” تابع ورودی که آن را تزئین میکند، جایگزینی (یا بستهبندی) تابع ورودی است.
به همین دلیل، تابع uppercase تابع دیگری را که بعدا میتوان فراخوانی کرد، را تعریف کرده و برمیگرداند، تابع ورودی اصلی را اجرا کرده و نتیجه آن را اصلاح میکند.
دکوریتورها رفتار تابع قابل فراخوانی را از طریق بستار بسته ساز (lexical closure) اصلاح میکنند، پس نباید تابع اصلی را به صورت دائم اصلاح کنند. تابع قابل فراخوانی اصلی به صورت دائمی اصلاح نمیشود بلکه فقط رفتار آن فقط هنگام فراخوانی تغییر میکند.
این کار به شما امکان اضافه کردن بلوک های ساختاری قابل استفاده مجدد، مانند ثبت وقایع و گزارش گیری و سایر ابزارهای دقیق، به توابع و کلاسهای موجود را میدهد. این کار دکوریتورها را به چنان ویژگی قدرتمندی در پایتون تبدیل میکند که اغلب در کتابخانه استاندارد و کتابخانههای شخص ثالث مورد استفاده قرار میگیرد.
یک وقفه کوتاه برای درک بهتر Decorator های پایتون
اگر احساس میکنید در این مرحله به اندازه یک فنجان قهوه یا پیاده روی در اطراف میز کارتان نیاز دارید، این کاملا طبیعی است. به نظر من، درک مفهوم دکوریتورها یکی از دشوارترین کارها در پایتون است.
لطفا عجله نکنید و نگران فهمیدن جواب مسئله نباشید. تکه کدهای بررسی شده را به صورت جداگانه در یک مفسر پایتون اجرا کنید و نتیجه ی آن را مشاهده کنید تا بهتر درک کنید. میدانم که میتوانید این کار را انجام دهید!
استفاده از چند Decorator روی یک تابع
شاید تعجب آور نباشد که میتوانید بیش از یک Decorator پایتون را روی تابع اعمال کنید. این کار اثرات آنها را انباشته میکند و این همان چیزی است که دکوریتورها را به اندازه بلوک های چندبخشی قابل استفاده مجدد سودمند میکند.
مثالی در این مورد بررسی خواهیم کرد. دو دکوریتور زیر، رشته خروجی تابع تزئین شده با برچسبهای HTML را بسته بندی میکنند. با نگاهی بر نحوه قرارگیری تو در توی تگ ها، میتوان فهمید که Decorator پایتون از چه فرآیندی برای اعمال چندین دکوریتور استفاده میکند.
def strong(func): def wrapper(): return '<strong>' + func() + '</strong>' return wrapper def emphasis(func): def wrapper(): return '<em>' + func() + '</em>' return wrapper
حال اجازه دهید این دو دکوریتور را انتخاب کنیم و آنها را در تابع greet خود اعمال کنیم. میتوانید از syntax@ معمولی برای آن استفاده کنید و چندین دکوریتور را در بالای یک تابع واحد انباشته کنید.
@strong @emphasis def greet(): return 'Hello!'
در صورت اجرای تابع تزیین شده، انتظار دارید کدام ورودی را ببینید؟ آیا دکوریتور emphasis@ ابتدا تگ <em> خود را اضافه خواهد کرد، یا strong@ اولویت دارد؟ هنگامی که تابع تزیین شده را فراخوانی میکنید، چه اتفاقی رخ خواهد داد؟
>>> greet() '<strong><em>Hello!</em></strong>'
این نتیجه به وضوح نشان میدهد که دکوریتورهای چند گانه چگونه از پایین به بالا عمل کردهاند. ابتدا تابع ورودی با دکوریتور emphasis@ بسته بندی شد و بعد تابع تزئین شده حاصل با دکوریتور strong@ دوباره بسته بندی شد.
برای اینکه رفتار پایین به بالا در حافظه تان باقی بماند میخواهم طریقه اجرای دکوریتورهای چندگانه را در هسته پایتون توضیح دهم. دکوریتورهای پایتون با استفاده از پشته پیاده سازی میشود و ما این رفتار را decorator stacking مینامیم. اولین پشته را در پایین ایجاد میکنیم و بعد بلوک های کد جدید را از بالا به آن اضافه میکنیم تا اجرای برنامه راه خود را از پایین به سمت بالا پیش ببرد.
اگر مثال بالا را تجزیه کنید و از syntax@ برای اعمال به دکوریتورها خودداری کنید، زنجیره فراخوانی های مربوط به تابع دکوریتور به این شکل خواهد بود:
decorated_greet = strong(emphasis(greet))
همانطور که میبینید ابتدا دکوریتور emphasis اعمال میشود و بعد تابع بسته بندی شده حاصل دوباره با دکوریتور strong بسته بندی میشود.
این همچنین به این معنی است که در نهایت سطوح عمیق پشته سازی دکوریتورها بر میزان کارایی اثر برنامه خواهند گذاشت، چون مدام فراخوانی های توابع تو در تو انجام میدهند. در عمل، معمولا انجام این کار مشکل نخواهد بود، اما اگر کارایی کد نوشته شده برایتان مهم است باید مرتبه زمانی عملیات decoration را در نظر بگیرید.
Decoration توابعی که آرگومانهای گوناگون میپذیرند
تاکنون همه نمونه ها فقط تابع ساده greet که هیچ آرگومانی ندارد را تزئین کرده اند. تا این لحظه، دکوریتورهایی که در این مطلب دیدید لازم نیست که با آرگومان های ارسال به تابع ورودی سر و کار داشته باشند. اگر یکی از این دکوریتورها را به تابعی که آرگومان هایی را میگیرد اعمال کنید، کارکرد درستی نخواهد داشت.
چگونه تابعی که آرگومانهای دلخواه میگیرد را تزئین میکنند؟
اینجاست که به ویژگی args* و kwargs** پایتون برای مقابله با تعداد متغیرهای آرگومان احتیاج خواهیم داشت. Decorator پایتون زیر به اسم پراکسی از این مزیت استفاده میکند.
def proxy(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
دو مورد قابل توجه در مورد این دکوریتور وجود دارد:
- با استفاده از عملگرهای * و ** در ورودی تابع wrapper، همه آرگومان های ورودی را جمع آوری میکند و آنها را به صورت شی (args و kwargs) ذخیره میکند.
- سپس تابع wrapper آرگومان های جمع آوری شده توسط عملکرد * و ** را بازگشایی میکند (argument unpacking) و آن را به تابع ورودی ارسال میکند.
امیدوارم ایده اصلی مسئله را متوجه شده باشید هرچند میدانم کمی نیاز به تمرین با کدها دارید تا ایده اصلی را به خوبی متوجه شوید.
اجازه دهید روش گذاشته شده با دکوریتور پراکسی را در مثالی عملی تر بسط دهیم. در اینجا دکوریتور trace را خواهیم ساخت که آرگومان های ورودی تابع و نتایج حاصل از آن را در طی زمان اجرا ثبت میکند.
def trace(func): def wrapper(*args, **kwargs): print(f'TRACE: calling {func.__name__}() ' f'with {args}, {kwargs}') original_result = func(*args, **kwargs) print(f'TRACE: {func.__name__}() ' f'returned {original_result!r}') return original_result return wrapper
تزئین تابع با تابع trace و بعد فراخوانی آن، آرگومان های منتقل شده به تابع و مقدار برگشتی آن را چاپ خواهد کرد.
این کار گاهی اوقات به عنوان یک کمک عالی برای خطایابی محسوب میشود.
@trace def say(name, line): return f'{name}: {line}' >>> say('Jane', 'Hello, World') 'TRACE: calling say() with ("Jane", "Hello, World"), {}' 'TRACE: say() returned "Jane: Hello, World"' 'Jane: Hello, World'
در رابطه با خطایابی صحبت کردیم، مواردی وجود دارند که هنگام خطایابی دکوریتورها باید به خاطر داشته باشید.
مهمترین نکته: نوشتن Decorator های قابل دیباگ!
هنگام استفاده از دکوریتور، در واقع تنها کاری که انجام میدهید جایگزین کردن یک تابع با تابع دیگر است. یک نکته منفی این فرآیند این است که برخی از metadata های تابع اصلی تزئین شده مخفی و جایگزین میشوند! این یکی از پنهانترین عملکردهای Decorator پایتون است که ممکن است درنظر گرفته نشود.
برای مثال، نام تابع اصلی، داکاسترینگ آن و لیست پارامترهای تابع پس از decorate تغییر خواهند کرد! به قطعه کد زیر توجه کنید.
def greet(): """Return a friendly greeting.""" return 'Hello!' decorated_greet = uppercase(greet)
در صورت دسترسی به هریک از metadata های تابع greet پس از decorate شدن، metadata مربوط به دکوریتور را مشاهده خواهید کرد که تغییر کردهاند.
>>> greet.__name__ 'greet' >>> greet.__doc__ 'Return a friendly greeting.' >>> decorated_greet.__name__ 'wrapper' >>> decorated_greet.__doc__ None
این کار خطایابی و کار با مفسر پایتون را چالش برانگیز میکند. خوشبختانه، برای این مشکل راهکاری سریع به نام دستور functools.wraps وجود دارد که در کتابخانه استاندارد پایتون گنجانده شده است.
میتوانید از دستور functools.wraps در دکوریتورهای خود استفاده کنید تا metadata های مفقود شده را از تابع تزئین نشده به تابع تزئین شده کپی کنید. مثالی را باهم بررسی میکنیم.
import functools def uppercase(func): @functools.wraps(func) def wrapper(): return func().upper() return wrapper
استفاده از دستور functools.wraps در دکوریتور باعث میشود metadata های مختلف هنگام decorate شدن تابع تغییری نکند و مقادیری مانند نام تابع و داکاسترینگ تابع به صورت دست نخورده باقی بماند. حقه ی جالبیست نه؟
@uppercase def greet(): """Return a friendly greeting.""" return 'Hello!' >>> greet.__name__ 'greet' >>> greet.__doc__ 'Return a friendly greeting.'
به عنوان بهترین روش، توصیه میکنم که در تمام دکوریتورهایی که مینویسید، از دستور functools.wraps استفاده کنید. این کار زیاد طول نمیکشد ولی باعث میشود شما و بقیه از دردسرهای خطایابی در دکورتیورها در امان بمانید.
تبریک میگویم، شما به انتهای این مطلب در رابطه با دکوریتورهای پایتون رسیدید و احتمالا درباره دکوریتورهای موجود در پایتون چیزهای زیادی آموخته اید. کارتان عالی بود!
نکات اصلی Decorator های پایتون
- دکوریتورها، بلوک های کد قابل استفاده مجدد هستند که میتوانید برای اصلاح رفتار یک تابع از آن استفاده کنید بدون اینکه نیاز به تغییر دائمی تابع باشد.
- دستور decorator@ تنها شکل کوتاه نویسی شده برای فراخوانی دکوریتور روی تابع ورودی است. دکوریتورهای متعدد روی تابع واحد از پایین به بالا اعمال میشوند (decorator stacking را به خاطر داشته باشید).
- به عنوان بهترین شیوه خطایابی، از دستور کمک کننده functools.wraps روی دکوریتورهای خود استفاده کنید تا metadata های بیشتری را از تابع قابل فراخوانی تزئین نشده (undecorated) به تابع قابل فراخوانی تزئین شده (decorated) منتقل کنید.
- دکوریتورها درست مانند هر ابزار دیگری در جعبه ابزار توسعه نرم افزار، نوشدارو یا علاج قطعی نیستند و نباید از آنها بیش از حد استفاده کرد. متعادل سازی نیازها بر حسب “اولویت انجام کارها” با هدف “گرفتار نشدن در آشفتگی وحشتناک و غیرقابل نگهداری کد منبع” بسیار اهمیت دارد.
عالی بود ??❤️
سلام و درود
خیلی ممنونم علیرضا جان?
خیلی جالب بودش مخصوصا آخرش
سلام و درود
سپاسگزارم دوست عزیز
مثل همیشه آموزشهای استاد عالیه.
سلام و درود
سپاسگزارم دوست عزیز، موفق باشید
فوق العاده بود
سلام
ممنونم از نظرتون دوست عزیز، موفق باشید
وای خیلی قشنگ و جالب بود
خیلی آسون و واضح توضیح دادید
آموزش async / await در پایتون هم قرار بدید
سلام، وقت بخیر
از نظرتون سپاسگزارم دوست عزیز، حتما در این مورد آموزشهایی خواهیم داشت.
موفق و پایدار باشید