Python حرفه‌ای: آشنایی با Decoratorها

Professional Python: Decorators

19 اسفند 1399
درسنامه درس 17 از سری پایتون حرفه‌ای
Python حرفه ای: آشنایی با decorator ها (قسمت 17)

decorator در پایتون چیست؟

اگر یادتان باشد در بخش برنامه نویسی شیء گرا و در هنگام تعریف کلاس ها با دستوراتی به نام classmethod@ و staticmethod@ آشنا شدیم. در آن جلسه به شما گفتم که فعلا ماهیت آن ها را نادیده بگیرید تا بعدا با آن ها آشنا شویم. قبل از اینکه بخواهیم به هویت decorator ها برسیم باید نکته مهمی را در رابطه با توابع یادآور شوم. در زبان پایتون توابع دقیقا مثل متغیرها هستند بنابراین می توانیم آن ها را به قسمت های مختلف برنامه پاس بدهیم. به مثال زیر توجه کنید:

def hello():

    print("helloooooo!!!")




another_variable = hello




print(another_variable)

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

<function hello at 0x7f8cbc19cdc0>

به عبارتی متغیر another_variable به محل تعریف تابع hello در مموری سیستم ما اشاره می کند. آیا می دانید این به چه معنی است؟ به کد زیر توجه کنید:

def hello():

    print("helloooooo!!!")







another_variable = hello




hello()

another_variable()

نتیجه اجرای این کد به شکل زیر است:

helloooooo!!!

helloooooo!!!

به عبارتی متغیر another_variable نیز همان تابع hello است و به محل آن در مموری اشاره می کند. در واقع ما با انجام این کار تعریف تابع hello را کپی نکرده ایم بلکه متغیر another_variable نیز به همان hello در مموری اشاره می کند. برای اثبات این موضوع می توانیم از دستور del استفاده کنیم. دستور del در پایتون یک متغیر یا تابع یا کلاس را حذف می کند. به مثال زیر توجه کنید:

def hello():

    print("helloooooo!!!")







another_variable = hello




del hello




hello()

من در اینجا تابع hello را حذف کرده و سپس آن را صدا زده ام. با اجرای این کد خطای زیر را می گیریم:

Traceback (most recent call last):

  File "/mnt/Development/Roxo Academy/Python/test.py", line 9, in <module>

    hello()

NameError: name 'hello' is not defined

این خطا می گوید که متغیری به نام hello وجود ندارد (یادتان باشد که توابع در پشت صحنه پایتون مانند متغیرها هستند). سوالی که از شما دارم این است: اگر در این حالت another_variable را صدا بزنیم چطور می شود؟

def hello():

    print("helloooooo!!!")







another_variable = hello




del hello




another_variable()

با اجرای کد بالا نتیجه زیر را دریافت می کنیم:

helloooooo!!!

بنابراین پایتون به اندازه کافی هوشمند است تا بداند با اینکه می خواهیم hello را حذف کنیم اما متغیر دیگری به نام another_variable را داریم که به محل تعریف hello در مموری اشاره می کند بنابراین فقط hello را حذف کرده اما تعریف تابع را در مموری نگه می دارد تا بتوانیم به another_variable آن را صدا بزنیم.

ما می توانیم این کد را یک مرحله به جلو ببریم و یک تابع را به عنوان آرگومان اول آن پاس بدهیم:

def greet(func):

    func()







def hello():

    print("HELLO!")







greet(hello)

همانطور که می بینید تابع greet پارامتر ورودی خود را به صورت یک تابع صدا می زند، بنابراین من می توانیم تعریف یک تابع دیگر (hello - بدون پرانتز) را به آن پاس بدهم تا آن را صدا بزند. با اجرای کد بالا نتیجه زیر را می گیریم:

HELLO!

احتمالا می پرسید این مباحث چه ربطی به decorator ها دارد؟ قابلیت هایی که در این بخش به شما نشان داده ام، سنگ بنای decorator ها هستند و هر decorator در پایتون با استفاده از همین قابلیت ها ساخته شده اند. کم کم در این دوره با decorator ها آشنا می شوید اما اگر بخواهم به صورت خلاصه بگویم decorator ها با استفاده از مباحثی که در همین بخش دیدیم، به دور توابع و کلاس ها پیچیده می شوند و قابلیت های آن ها را گسترش می دهند، بدون اینکه نیازی به ویرایش خود تابع یا کلاس داشته باشیم. قبل از اینکه بخواهم به صورت عملی با decorator ها دست و پنجه نرم کنیم باید با مفهوم higher order function آشنا شویم.

higher order function چیست؟

higher order function (به معنی «توابع بالا مرتبه» و به صورت خلاصه HOC) به توابعی گفته می شود که در یکی از دو دسته زیر قرار بگیرند:

  • تابعی که یک تابع دیگر را به عنوان آرگومان خود قبول می کند. چیزی که در همین بخش با توابع hello و greet مشاهده کردیم.
  • تابعی که یک تابع دیگر را برمی گرداند.

یک مثال ساده از دسته اول به شکل زیر است:

def greet(func):

    func()

ما قبلا نیز این تابع را دیده بودیم بنابراین توضیح بیشتری در مورد آن نمی دهم. مثالی دیگر برای دسته دوم HOC ها نیز می تواند به شکل زیر باشد:

def my_function():

    def another_function():

        print("THIS IS THE INSIDE FUNCTION")

    return another_function

همانطور که می بینید ما در این تابع یک تابع دیگر را تعریف کرده و سپس آن را برگردانده ایم بنابراین my_function یک HOC محسوب می شود. توجه داشته باشید که ما another_function را return کرده ایم و هیچ پرانتزی را در مقابل نام آن نگذاشته ایم تا درون این تابع اجرا نشود. برای اجرای آن می توانیم بدین شکل عمل کنیم:

def my_function():

    def another_function():

        print("THIS IS THE INSIDE FUNCTION")

    return another_function







my_variable = my_function()




my_variable()

با صدا زدن my_function تعریف تابع another_function را return کرده و به متغیر my_variable منسوب می کنیم. توجه داشته باشید که فقط تعریفش را به my_variable منسوب کرده ایم اما هنوز آن را اجرا نکرده ایم. برای اجرای آن مانند یک تابع ساده صدایش می زنیم. با اجرای کد بالا نتیجه زیر را دریافت خواهید کرد:

THIS IS THE INSIDE FUNCTION

بنابراین تابع my_function یک تابع HOC یا بالا مرتبه حساب می شود. حتما با خودتان می گویید پس توابعی مانند map و reduce و filter نیز HOC هستند چرا که یک تابع را به عنوان آرگومان خود دریافت می کنند. حرفتان کاملا صحیح است! تمامی این توابع از نوع HOC بوده و در پشت صحنه پایتون بدین شکل نوشته شده اند.

ساخت یک decorator ساده

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

def my_decorator(func):

    def wrap_function():

        func()

    return wrap_function

احتمالا می گویید چرا باید چنین تابعی بنویسیم؟ به دلیل اینکه می توانیم از آن به عنوان یک decorator در پایتون استفاده کنیم! ما فعلا در آن کار خاصی انجام نداده ایم اما صبور باشید. برای استفاده از تابع my_decorator به عنوان یک decorator باید از علامت @ استفاده کنیم:

def my_decorator(func):

    def wrap_function():

        func()

    return wrap_function







@my_decorator

def hello():

    print("HELLO!!")







hello()

برای اعمال یک decorator روی یک تابع بای نامش را با علامت @ و یک خط بالاتر از تابع خودتان صدا بزنید (دقیقا مانند کد بالا). طبیعتا با اجرای این کد همان رشته !!HELLO را دریافت می کنید اما هنوز جای سوال است که چرا برای اجرای یک تابع ساده چنین کد طولانی را نوشتیم. من برای ساده بودن درک کدها کد خاصی را در decorator خود ننوشته ام اما decorator های واقعی مقدار خاصی داشته و تابع اصلی را تغییر می دهند. به طور مثال:

def my_decorator(func):

    def wrap_function():

        print("***********")

        func()

        print("***********")

    return wrap_function







@my_decorator

def hello():

    print("HELLO!!")







hello()

با اجرای این کد نتیجه زیر را می گیریم:

***********

HELLO!!

***********

همانطور که می بینید تابع hello ما با اضافه کردن decorator در پایتون بهتر شده است. کلمه decorator به معنای تزئین کننده است و حتما حالا معنای آن را درک می کنید. با اینکه این مثال یک مثال ساده و ابتدایی بود اما کارایی اصلی decorator ها را برایمان توضیح می دهد. از این به بعد هر زمانی که بخواهیم دستور print خود را درون علامت های ستاره قرار بدهیم می توانیم بدون تغییر دادن سورس کد تابع با یک decorator این کار را انجام بدهیم. بیایید کمی بیشتر در مورد جزئیات باقی مانده و ضرورت و کاربرد decorator ها صحبت کنیم.

آشنایی با decorator pattern

در این قسمت می خواهم از شما سوالی بپرسم. آیا می توانیم آرگومان مورد نظر خودمان را به تابع hello در کد زیر پاس بدهیم؟

def my_decorator(func):

    def wrap_function():

        print("***********")

        func()

        print("***********")

    return wrap_function







@my_decorator

def hello(some_data):

    print(some_data)







hello()

همانطور که می بینید من پارامتری به نام some_data را به تعریف این تابع پاس داده ام. اگر این کد را اجرا کنیم خطای زیر را می گیریم:

***********

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 14, in <module>

    hello()

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 4, in wrap_function

    func()

TypeError: hello() missing 1 required positional argument: 'some_data'

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

def my_decorator(func):

    def wrap_function(x):

        print("***********")

        func(x)

        print("***********")

    return wrap_function







@my_decorator

def hello(some_data):

    print(some_data)







hello('this is my data')

من نام این پارامتر را x گذاشته ام اما شما می توانید هر نام دیگری برایش بگذارید. با انجام این کار، آرگومان ما از طریق wrap_function به تابع hello پاس داده می شود و نتیجه زیر را می گیریم:

***********

this is my data

***********

اگر بخواهم کد بالا را ساده تر کنم، می گویم decorator ما تابع hello را به صورت یک آرگومان قبول می کند:

def my_decorator(func):

    def wrap_function(x):

        print("***********")

        func(x)

        print("***********")

    return wrap_function







def hello(some_data):

    print(some_data)







a = my_decorator(hello)

a('hello there')

این کد دقیقا همان نتیجه قبلی را به ما می دهد بنابراین متوجه می شویم که decorator ها در پشت صحنه بدین شکل کار می کنند. از آنجایی که از ابتدای این جلسه مفصلا در مورد نحوه اجرای این کدها توضیح دادیم، دیگر توضیح بیشتری نمی دهم.

مسئله بعدی این است که اگر تابع hello علاوه بر some_data پارامتر دیگری را نیز بگیرد، چطور؟ آنگاه مجبور هستیم wrap_function درون decorator خود را ویرایش کنیم و علاوه بر x یک آرگومان دیگر را نیز پاس بدهیم. این فرآیند، اصلا جالب نیست چرا که هر بار ما را مجبور به ویرایش wrap_function می کند. علاوه بر این اگر یک پارامتر کلیدواژه ای داشته باشیم موقعیت بدتر می شود. برای حل این مشکل روش بسیار بهتری وجود دارد و آن استفاده از args* است:

def my_decorator(func):

    def wrap_function(*args, **kwargs):

        print("***********")

        func(*args, **kwargs)

        print("***********")

    return wrap_function







@my_decorator

def hello(some_data, emoji=":)"):

    print(some_data, emoji)







hello("SOME_DATA)

ما قبلا به طور جزئی در مورد استفاده از args* و kwargs* صحبت کرده ایم بنابراین نیازی به توضیحات اضافه نیست. با استفاده از این روش تنها متد های خودمان (hello) را ویرایش می کنیم و decorator نیازی به ویرایش نخواهد داشت. با اجرای این کد نتیجه زیر را دریافت می کنیم:

***********

SOME_DATA :)

***********

الگویی که در این بخش برای کدنویسی decorator خود انتخاب کردیم به decorator pattern معروف است. منظور من دقیقا الگوی زیر است:

def my_decorator(func):

    def wrap_function(*args, **kwargs):

        func(*args, **kwargs)

    return wrap_function

شما هر زمان که بخواهید یک decorator را تعریف کنید می توانید کد بالا را کپی و paste نمایید.

کاربردهای عملی decorator ها

شاید با خودتان بگویید با این همه هنوز کاربرد عملی decorator ها برای ما مشخص نشده است. جدا از اینکه ما decorator های پیش فرض زبان پایتون مانند classmethod@ و staticmethod@ را داریم و در جلسات برنامه نویسی شیء گرا از آن ها استفاده کردیم، می توانیم decorator هایی را مخصوص برنامه های خودمان بسازیم. به طور مثال فرض کنید که ما در حال تست کردن کدهایمان هستیم و می خواهیم بدانیم سرعت اجرای یک تابع چقدر است. به طور مثال فرض کنید تابع زیر را داشته باشیم:

def long_time():

    for i in range(100000000)

    i * 5

این تابع یک حلقه است که یک میلیون بار اجرا می شود و اعداد ۰ تا ۱ میلیون را در ۵ ضرب می کند. ما می خواهیم بدانیم زمان لازم برای اجرای این تابع در سیستم خودمان چقدر است. در این حالت دو راه داریم: کدهای اندازه گیری زمان را درون این تابع بنویسیم یا یک decorator خاص برای اندازه گیری زمان اجرای تابع بنویسیم تا بتوانیم از آن روی هر تابع دیگری نیز استفاده کنیم! من شروع به نوشتن decorator می کنم:

from time import time







def performance(fn):

    def wrapper(*args, **kwargs):

        t1 = time()

        result = fn(*args, **kwargs)

        t2 = time()

        print(f'took {t2 - t1} seconds')

        return result

    return wrapper







@performance

def long_time():

    for i in range(100000000):

        i * 5







long_time()

در ابتدای این کد ماژول شیء time از ماژول time را وارد کدهایمان کرده ایم. قبلا هم چنین کاری را کرده بودیم و مانند همان جلسه باز هم به شما می گویم که فعلا ماژول ها را نادیده بگیرید تا به بخش ماژول ها در پایتون برسیم. تنها کاری که این خط از کد انجام می دهد این است که تابعی به نام time را وارد کدهای ما می کند تا بتوانیم از آن استفاده کنیم. در قسمت بعدی شروع به تعریف decorator خود کرده و از همان الگوی decorator pattern استفاده می کنیم اما این بار قسمت هایی اضافی نیز داریم. صدا زدن تابع ()time زمان فعلی را به ما می دهد بنابراین می توانیم یک بار آن را قبل از اجرای تابع و یک بار بعد از اجرای تابع صدا بزنیم. در نهایت با تفریق این دو عدد از هم (تفاوت زمان) می توانیم زمان صرف شده برای اجرای توابع خود را به دست بیاوریم. من نتیجه تابع را نیز در متغیری به نام result گذاشته و در انتها return کرده ام. چرا؟ این decorator قرار است روی هر تابعی اجرا شود و برخی از توابع مقدار خاصی تولید می کنند بنابراین باید آن را برگردانیم. حالا این decorator را به تابع long_time اضافه می کنیم که درون خود حلقه ای دارد که ۱۰۰ میلیون گردش انجام می دهد. من نتیجه ای را درون این تابع چاپ نمی کنم چرا که اگر ۱۰۰ میلیون نتیجه را چاپ کنیم، سیستم ما به شدت درگیر شده و ممکن است crash کند. با اجرای این تابع نتیجه زیر را دریافت می کنیم:

took 5.975167512893677 seconds

به عبارتی اجرای این تابع ۵.۹ ثانیه طول کشیده است. ما می توانیم این decorator را به هر تابعی در برنامه خود اضافه کنیم و زمان لازم برای اجرای آن را اندازه گیری کنیم.

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

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.