اشکان محمدی
اشکان محمدی
خواندن ۲ دقیقه·۴ سال پیش

دکوریتور ها در پایتون #2

نقش دکوریتور در تغییر عملکرد تابع
نقش دکوریتور در تغییر عملکرد تابع

سلام؛ در پست قبلی درباره دکوریتور های پایتون یادگرفتیم و دیدم که چطوری یک دکوریتور با توابع تو در تو (nested functions) بسازیم. توی این پست می خوام روی دکوریتور ها بیشتر زوم کنم.

می خوام با یه مثال خیلی ساده شروع کنم، فرض کنید که همچین تابع و دکوریتوری داریم:

def header(function): def wrapper(*args, **kwargs): return f&quot<h1> {} </h1>&quot return wrapper @header def getTitle(): return &quotHello this is the title&quot

اگر تابع getTitle را فراخوانی کنیم، این نتیجه را خواهیم گرفت:

<h1> Hello this is the title </h1>

دکوریتور های آرگومان دار

شاید برای ما فقط تگ h1 کافی نباشه و بخواهیم که مثلا تمام تگ های h1 الی h6 رو پشتیبانی کنیم. در این موقعیت دو تا راه حل وجود داره.

  1. برای هر کدوم یه دکوریتور جداگانه بنویسیم، که فقط اتلاف وقته
  2. به دکوریتورمون آرگومانی اضافه کنیم که تعیین کننده level تگ باشه. (راه حل بهتر*)

ولی چطوری باید این کار رو بکنیم؟ اصلا نگران نباشید الان بهتون نشون میدم. قدم اول اینه که به جای 2 تا تابع تو در تو (که دکوریتور و رپر (wrapper) رو تشکیل میدن)، 3 تابع تو در تو داشته باشیم:

def header(level: int): if level < 1 or level > 6: raise Exception(&quotthe level argument should be a value between 1 and 6&quot) def decorator(function): def wrapper(*args, **kwargs): return f&quot<h{level}> {function(*args, **kwargs)} </h{level}&quot return wrapper return decorator

شاید کمی پیچیده شده باشه، الان از این دکوریتور به این شکل استفاده میشه:

@header(3) # this is how the arguments are passed, note the parenthesis def getTitle(): return &quotHello this is the title!&quot

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

getTitle()

چیزی که مفسر می فهمه

header(3)(getTitle)()

البته همچین چیزی اتفاق نمی افته، وقتی شما تابع رو دکوریت می کنید، همون زمانی که تابع تعریق میشه، به دکوریتور داده میشه و wrapper برگردونده میشه، پس وقتی که شما تابع رو صدا بزنید، در اصل wrapper رو صدا زدید ولی من برای فهم بهتر اینطوری می نویسم

نتیجه نهایی:

<h3> Hello this is the title </h3>

همونطوری که می بینید در اصل header با آرگومان 3 به عنوان levelش صدا زده میشه. بعد از اینکه تابع چک می کنه که level بین 1 الی 6 هست یا نه، تابع داخلیشو (decorator رو) بر می گردونه و در مرحله بعدی خود تابع getTitle به decorator داده میشه. تابع decorator هم wrapper رو بر می گردونه که آرگومان های احتمالی getTitle رو بگیره. در نهایت تابع wrapper خروجی تابع رو دستکاری می کنه و بر می گردونه.

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

یه مشکل کوچیک

کد های زیر رو در نظر بگیرید:

def header(function): def wrapper(*args, **kwargs): &quot&quot&quot this is docstring of wrapper &quot&quot&quot return f&quot<h1> {function(*args, **kwargs)} </h1>&quot return wrapper @header def getTitle(): &quot&quot&quot this is doc string of getTitle function &quot&quot&quot return &quotHello this is the title&quot

تو این مثال برگشتیم سر مثال ساده ای که اول پست زدم، اما با این تفاوت که این دفعه docstring ها رو هم توی هر تابع داریم. نقش docstring ها اینه که توضیحاتی رو درباره نحوه عملکرد تابع ارائه میدن. اما اینجا یه نکته ای وجود داره. اگه ترمینالتون رو باز کنید و این کد های بالا رو به ترتیب توش وارد کنید و بعد این کد رو بنویسید، نتیجه جالبه

>>> getTitle <function __main__.header.<locals>.wrapper(*args, **kwargs)>

همونطور که می بینید، در اصل تابع getTitle همون wrapper هستش که از توابع داخلی (local) دکوریتور header هستش. حالا این کد رو اجرا کنید:

>>> getTitle.__doc__

یادتونه که یک سری docstring توی تابع وجود داشت، اون docstring ها توی __doc__ ذخیره میشن. شاید انتظار داشته باشید همون docstrnigای رو که توی تابع getTitle بود مشاهده کنید، اما:

' this is doc string of getTitle function ' # this is what you expect ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ' this is doc string of wrapper function ' # <- actual results

و این یعنی خصوصیاتی که مربوط به getTitle هستند از بین میروند (همونطور که دیدید حتی اسم تابع هم به wrapper تغییر پیدا کرده بود). برای اینکه این فاجعه رو برطرف کنیم، باید از ماژول functools پایتون کمک بگیریم. اطلاعات بیشتر راجب این ماژول را در اینجا پیدا کنید. فعلا از اینهمه امکانات فقط دکوریتور wraps رو نیاز داریم.

from functools import wraps

و حالا فقط یه تغییر کوچیک توی دکوریتور ایجاد می کنیم:

def header(function): @wraps(function) # + decorate the wrapper with wraps decorator def wrapper(*args, **kwargs): # the rest of code .....

کاری که wraps انجام میده اینه که چیز هایی مثل __doc__, __name__ و ... که مربوط به function هستند رو به wrapper میده و با اینکار باعث میشه که wrapper شبیه خود تابعی که دکوریت شده به نظر بیاد. (wrapper رو تبدیل به تابع اصلی که دکوریت شده نمی کنه اما خصوصیاتش رو به اون نسبت میده)

حالا با اجرای این کد docstring مربوط به خود تابع getTitle را خواهیم دید

getTitle.__doc__ # output: ' Hello this is doc string of getTitle function '

خوب خدارا شکر مشکل حل شد.



نمی خواهم پست زیاد طولانی بشه و همینجا خاتمه اش میدهم. اما هنوز تمام نشده. منتظر مطلب بعدی من درباره دکوریتور های پایتون باشید.

برای کسب اطلاعات بیشتر راجب دکوریتور ها به این مطلب از وبسایت realpython مراجعه کنید

دکوریتورپایتونdecoratorpython
یه برنامه نویس ساده که از تجربیات و آموخته هاش می نویسه
شاید از این پست‌ها خوشتان بیاید