محمد رضائی
محمد رضائی
خواندن ۵ دقیقه·۴ سال پیش

Python Decorators


خب امروز میخوام در مورد decorator توی زبان پایتون کمی توضیح بدم تا یک بار برای همیشه نحوه کار با اون رو یاد بگیریم. اما بریم ببینیم اول تعریف decorator چی هست:

زمانی که ما میخوایم رفتار یک موجودیت (تابع و کلاس و...) رو دستخوش یک سری تغییرات کنیم بدون این که نیاز باشه هر بار اون تغییرات رو به بدنه تابع یا کلاس موجود اضافه کنیم decorator به کمک ما میاد تا این کار رو راحت انجام بدیم و نیازی به دوباره کاری هم نباشه. در واقع decorator در قالب یک wrapper اون تغییرات رو به تابع شما اضافه میکنه.

برای شروع کار از یک مثال ساده استفاده میکنیم. تابع زیر صرفا قرار یک رشته ثابت رو چاپ کنه:

def say_hi(): print(&quothello!&quot)

خب حالا ما میخوایم یک decorator ساده بنویسیم که یک تغییرات کوچیک توی نحوه عملکرد برای ما انجام بده:

def my_decorator(func): def wrapper(): print(&quotSomething is happening before the function is called.&quot) func() print(&quotSomething is happening after the function is called.&quot) return wrapper

اما همونطور که توی کد بالا می بینید my_decorator به عنوان آرگومان ورودی یک تابع رو دریافت میکنه چرا که قرار تغییراتی رو روی عملکرد همون تابع انجام بده و در قالب یک تابع (که اینجا من اسمش رو warpper گذاشتم و البته هر اسمی میتونه داشته باشه) اون رو به ما برگردونه. خب حالا برای اینکه این decorator رو به تابع ساده قبلی مون اضافه کنیم به دوشکل میتونیم عمل کنیم:

say_hi = my_decorator(say_hi)

روش دوم اینه که با یک علامت @ بعلاوه اسم decorator اون رو قبل تابع اضافه کنیم به شکل زیر:

@my_decorator def say_hi(): print(&quothello!&quot)

به همین سادگی حالا با اجرای تابع خروجی زیر را خواهیم داشت:

>>> say_hi() Something is happening before the function is called. hello! Something is happening after the function is called.

خب همون طور که دیدید تغییرات decorator روی تابع ما اعمال شد. اما حالا اگر تابع ما آرگومان هم داشت چی ؟ پس بریم سراغ یک مثال دیگه که آرگومان هم داشته باشه . اول این بار decorator زیر رو در نظر بگرید که صرفا قرار یک تابع رو ۲ بار اجرا کنه :

def do_twice(func): def wrapper_do_twice(): func() func() return wrapper_do_twice

حالا decorator بالا رو میخوایم روی تابع زیر که آرگومان هم داره اعمال کنیم:

@do_twice def greet(name): print(f&quotHello {name}&quot)

خب الان بنظرتون اگر تابع مون رو اجرا کنیم خروجیش چی هستش ؟؟!

>>> greet(&quotWorld&quot) Traceback (most recent call last): File &quot<stdin>&quot, line 1, in <module> TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

بله ارور میگیریم میپرسید چرا ؟ به خاطر اینکه آرگومان های مورد انتظار رو بهش پاس ندادیم. خب حالا decorator رو بازنویسی میکنیم و آرگومان های مورد انتظارش رو بهش ارسال میکنیم:

def do_twice(func): def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) func(*args, **kwargs) return wrapper_do_twice

حالا دیگه میتونیم خروجی درست رو ازش بگیریم:

>>> greet(&quotWorld&quot) Hello World Hello World

خب تا اینجا آرگومان هم ارسال کردیم به decorator و جواب هم گرفتیم اما اگر بخوایم یک مقدار خروجی هم return بشه از اون چیکار باید کنیم. پس بریم سراغش با مثال زیر:

@do_twice def return_greeting(name): print(&quotCreating greeting&quot) return f&quotHi {name}&quot

خب این تابع یک داره یک مقدار رو برمیگردونه و انتظار داریم بتونیم با اجرای اون مقدار رو دریافت کنیم پس بریم اجراش کنیم:

>>> hi_adam = return_greeting(&quotAdam&quot) Creating greeting Creating greeting >>> print(hi_adam) None

خب دیدید برای مقدار خروجی چیزی جز None نصیبمون نشد. خب چرا؟ به خاطر اینکه توی decorator مقداری رو return نکردیم حالا بزن بریم decorator رو اصلاح کنیم تا جواب بگیریم :

def do_twice(func): def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) return func(*args, **kwargs) return wrapper_do_twice

و حالا ببینیم جواب میگیریم یا نه:

>>> return_greeting(&quotAdam&quot) Creating greeting Creating greeting 'Hi Adam'

خب خیالمون راحت شد جواب رو گرفتیم.

حالا برای اینکه حق مطلب هم ادا بشه یک نکته مهم رو هم میخوام مطرح کنم تا در استفاده از decorator حواستون بهش باشه. ببینید شما توی کنسول پایتون وقتی نام یک تابع (بدون نیاز به پرانتز و آرگومان ها)رو بزنید هویت (identity) اون تابع رو بهتون نشون میده وبا __name__ میتونید نام اون تابع رو داشته باشید یا با استفاده از __doc__ میتونید docstring مربوط به همون تابع رو داشته باشید (در مورد help هم به همین شکل) دقیقا مثل مورد زیر که همین موارد رو برای تابع print در پایتون نشون میده :

>>> print <built-in function print> >>> print.__name__ 'print' >>> help(print) Help on built-in function print in module builtins:

حالا اگه همین موارد رو برای تابع greet که قبلتر نوشتیم اجرا کنیم ببینیم چی میشه:

>>>greet
<function do_twice.<locals>.wrapper_do_twice at 0x104021268>
>>>greet.__name__
'wrapper_do_twice'
>>>help(greet)
Help on function wrapper_do_twice in module __main__:
wrapper_do_twice(*args, **kwargs)

عجیبه! چرا اینجوری شد پس؟ جوابش خب سادست چون identity اون تابع ما با warpper ای که توی decorator نوشتیم جایگزین شده حتی نام تابع هم درست نیست و به جاش نام wrapper ی که نوشتیم یعنی wrapper_do_twice رو دریافت کردیم.

خب حالا چطور این مشکل رو حل کنیم. به کمک یکی decorator ها در پایتون که به همین منظور پیاده سازی شده میتونیم این مشکل رو حل کنیم و اگر decorator مون رو به شکل زیر تغییر بدیم این مسئله حل خواهد شد:

import functools def do_twice(func): @functools.wraps(func) def wrapper_do_twice(*args, **kwargs): func(*args, **kwargs) return func(*args, **kwargs) return wrapper_do_twice

خب کاری که functools.wraps انجام میده این که میاد تمام اطلاعات مربوط به تابع اصلی رو در decorator حفظ میکنه. حالا ببینیم که خروجی بعد از این تغییر چی شد:

>>>greet
<function greet at 0x10c43ebf8>
>>>greet.__name__
'greet'
>>>help(greet)
Help on function greet in module __main__:
greet(name)

خب حالا همه چی به روال درستش برگشت.

جمع بندی‌

من سعی کردم روش استفاده از decorator رو برای شروع کار خیلی ساده بیان کنم تا خواننده گیج نشه و بتونه کار با اون ها یاد بگیره بعد خودش بره سراغ موارد پیچیده تر. امیدوارم که این مطلب براتون مفید واقع شده باشه.

منبع :

https://realpython.com/primer-on-python-decorators












pythondecoratordesign pattern
یک بک اند دولوپر مشتاق یادگیری و البته یاد دادن
شاید از این پست‌ها خوشتان بیاید