سلام؛ در پست قبلی درباره دکوریتور های پایتون یادگرفتیم و دیدم که چطوری یک دکوریتور با توابع تو در تو (nested functions) بسازیم. توی این پست می خوام روی دکوریتور ها بیشتر زوم کنم.
می خوام با یه مثال خیلی ساده شروع کنم، فرض کنید که همچین تابع و دکوریتوری داریم:
def header(function): def wrapper(*args, **kwargs): return f"<h1> {} </h1>" return wrapper @header def getTitle(): return "Hello this is the title"
اگر تابع getTitle را فراخوانی کنیم، این نتیجه را خواهیم گرفت:
<h1> Hello this is the title </h1>
شاید برای ما فقط تگ h1 کافی نباشه و بخواهیم که مثلا تمام تگ های h1 الی h6 رو پشتیبانی کنیم. در این موقعیت دو تا راه حل وجود داره.
ولی چطوری باید این کار رو بکنیم؟ اصلا نگران نباشید الان بهتون نشون میدم. قدم اول اینه که به جای 2 تا تابع تو در تو (که دکوریتور و رپر (wrapper) رو تشکیل میدن)، 3 تابع تو در تو داشته باشیم:
def header(level: int): if level < 1 or level > 6: raise Exception("the level argument should be a value between 1 and 6") def decorator(function): def wrapper(*args, **kwargs): return f"<h{level}> {function(*args, **kwargs)} </h{level}" return wrapper return decorator
شاید کمی پیچیده شده باشه، الان از این دکوریتور به این شکل استفاده میشه:
@header(3) # this is how the arguments are passed, note the parenthesis def getTitle(): return "Hello this is the title!"
حالا وقتی شما تابع رو فرا خوانی می کنید. چیزی که شما می نویسید:
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): """ this is docstring of wrapper """ return f"<h1> {function(*args, **kwargs)} </h1>" return wrapper @header def getTitle(): """ this is doc string of getTitle function """ return "Hello this is the title"
تو این مثال برگشتیم سر مثال ساده ای که اول پست زدم، اما با این تفاوت که این دفعه 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 مراجعه کنید