دکوراتورها در پایتون و نحوه پیاده‌سازی آن‌ها

دکوراتورها در پایتون و نحوه پیاده‌سازی آن‌ها

دکوراتور ها (Python Decorators) ابزاری قدرتمند و مفید در پایتون هستند؛ چراکه به برنامه‌نویس‌ها این اجازه را می‌دهند که رفتارهای تابع یا کلاسشان را بهبود دهند. دکوراتورها نوعی الگوی طراحی (Design pattern) هستند که به کاربر اجازه می‌دهند به یک شی از قبل موجود، بدون اینکه در ساختارش تغییری ایجاد شود، یک عملکرد جدید اضافه کند. در این مطلب به این می‌پردازیم دکوراتورها چه هستند و چطور می‌توانیم از دکوراتورها در توابع پایتون استفاده کنیم. 

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

توابع در پایتون 

قبل از اینکه دکوراتورها را بفهمیم، لازم است ابتدا بدانیم توابع چگونه کار می‌کنند. تابعی را در نظر بگیرید که یک آرگومان می‌گیرد و بر اساس آن مقداری را برمی‌گرداند: 

def add_one(number):
    return number + 1

>>> add_one(2)

3

توابع در پایتون می‌توانند به عنوان آرگومان به تابع دیگری داده شوند، به عنوان خروجی تابع دیگری ساخته شوند، بهبود داده شوند و به یک متغیر اساین شوند (assign). 

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

def plus_one(number):
return number + 1

add_one = plus_one
>>> add_one(5)

6

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

def shout(text):
    return text.upper()
 
def whisper(text):
    return text.lower()
 
def greet(func):
    greeting = func("""Hi, I am created by a function passed as an argument.""")
    print (greeting)
 
>>> greet(shout)
>>> greet(whisper)

HI, I AM CREATED BY A FUNCTION PASSED AS AN ARGUMENT.
hi, i am created by a function passed as an argument.

در مثال بالا، تابع greet، یک تابع دیگر را به عنوان پارامتر می‌پذیرد (توابع whisper و shout). 

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

def create_adder(x):
    def adder(y):
        return x+y
 
    return adder
 
add_15 = create_adder(15)
 
>>> print(add_15(10))

25

در مثال بالا، یک تابع را درون تابع دیگری ایجاد کردیم و آن را به عنوان خروجی تابع اول بازگرداندیم. مثال‌های بالا برای درک بهتر دکوراتورها لازم بودند. حالا می‌توانیم به دکوراتورها در پایتون بپردازیم.

دکوراتورها 

هدف از دکوراتورها بهبود عملکرد توابع یا کلاس‌ها است. در دکوراتورها، توابع به عنوان آرگومان به تابع دیگری داده می‌شوند و بعد درون تابع wrapper فراخوانی می‌شوند. در ادامه با یک مثال منظورمان روشن‌تر می‌شود:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

حالا باید تابع say_whee را فراخوانی کنیم:

>>> say_whee()

Something is happening before the function is called.
Whee!
Something is happening after the function is called.

بیایید ببینیم چه اتفاقی افتاد. همانطور که گفتیم، این امکان وجود دارد که تابعی را به عنوان آرگومان به یک تابع دیگر بدهیم. درست همان کاری که در این خط انجام داده‌ایم:

say_whee = my_decorator(say_whee)

عملیات استفاده از دکوراتور در همین خط اتفاق می‌افتد. در واقع، say_whee حالا به تابع داخلی، یعنی همان تابع wrapper اشاره دارد. و نباید فراموش کنیم که وقتی my_decorator(say_whee) را صدا می‌زنیم، تابع wrapper را به عنوان خروجی باز می‌گردانیم. در نهایت، ()say_whee() ،wrapper را به عنوان یک تابع صدا می‌زند و قبل و بعد از آن دو بار ()print را فراخوانی می‌کند. همانطور که در ابتدا گفتیم، دکوراتور رفتار یک تابع را تغییر می‌دهد. در این مثال، تابع say_whee با تغییراتی مورد استفاده قرار گرفت. 

قبلا از اینکه به سینتکس (syntax) مخصوص دکوراتورها برسیم، بیاید نگاهی به یک مثال کاربردی‌تر بیندازیم.

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)

در این مثال، باز هم تابع say_whee را داریم، که عبارتی را پرینت می‌کند. با استفاده از دکوراتور این شرط را اعمال کرده‌ایم که در طول روز عبارت چاپ شود، اما در طول شب این اتفاق نیفتد. به این معنی که اگر در ساعت‌های قبل از ۷ صبح و بعد از ۱۰ شب تابع دکوراتور را فراخوانی کنیم، پاسخی دریافت نخواهیم کرد. 

سینتکس دکوراتورها 

در بخش قبل، نحوه کار دکوراتورها را شرح دادیم، که به همان شیوه هم قابل پیاده‌سازی است. روش ساده‌تر و متداول‌تر پیاده‌سازی دکوراتورها، استفاده از نماد @ است. بیایید یک بار دیگر ببینیم که مثال اول‌مان برای دکوراتورها، چگونه با استفاده از @ اجرا می‌شود: 

def my_decorator(func):
    def wrapper():
       print("Something is happening before the function is called.")
        func()
       print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_whee():
    print("Whee!")

در واقع my_decorator@، روش ساده‌تر نوشتن این خط کد است:‌

say_whee = my_decorator(say_whee)

استفاده دوباره از دکوراتورها

نباید فراموش کنیم که دکوراتورها هم دقیقا مانند سایر توابع معمولی پایتون هستند. پس تمام ابزارهای استفاده دوباره آسان از توابع در پایتون برای دکوراتورها هم در دسترس است. بیایید مثالی ببینیم که در آن از دکوراتور در توابع دیگر استفاده شده است. برای این کار فایلی به اسم decorators.py می‌سازیم و کد زیر را در آن ذخیره می‌کنیم:‌

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

نکته: شما می‌توانید اسم تابع داخلی را هرچیزی بگذارید و معمولا اسمی کلی مانند ()wrapper مشکلی ایجاد نمی‌کند. در این مطلب برای اینکه مثال‌هایی که آورده‌ایم قابل تمایز باشند، بعد از wrapper از یک پسوند هم استفاده می‌کنیم. 

برای استفاده از دکوراتوری که در فایل ذخیره‌ کردیم، کافی است دکوراتور را در کدی که می‌خواهیم از آن استفاده کنیم import کنیم:

from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")
و با اجرای این کد، نتیجه زیر را خواهیم داشت:‌

>>> say_whee()
Whee!
Whee!

استفاده از دکوراتورها با آرگومان

اگر تابعی آرگومان بپذیرد، باز هم می‌توانیم به عنوان دکوراتور از آن استفاده کنیم؟ بهتر است امتحان کنیم: 

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")

متاسفانه با اجرای این کد به خطا برمی‌خوریم:

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

مشکل اینجاست که تابع داخلی، یعنی همان ()wrapper_do_twice، هیچ آرگومانی نمی‌پذیرد، اما name="World" به آن پاس داده شده است. این مشکل را می‌توانیم به این صورت حل کنیم که اجازه دهیم تابع ()wrapper_do_twice آرگومان بپذیرد. اما در این صورت اگر تابع داخلی ما ()say_whee باشد، که قبلا مثال آن را دیدیم، باز هم به مشکل برمی‌خوریم.

راه‌حل این است که در تابع داخلی wrapper، از  args* و kwargs** استفاده کنیم. با این کار تابع داخلی، به تعداد دلخواه آرگومان و کیورد می‌پذیرد. پس کافی است فایل decorators.py را به صورت زیر بازنویسی کنیم:‌

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

💎 بیشتر بدانید: آشنایی با برخی از پرکاربردترین قابلیت‌های زبان برنامه‌نویسی پایتون

حالا تابع داخلی ()wrapper_do_twice آرگومان می‌پذیرد و آن‌ها را به تابع دکوراتور پاس می‌دهد. در نتیجه هر دو مثال قبلی ما به درستی کار خواهند کرد: 

>>> say_whee()
Whee!
Whee!

>>> greet("World")
Hello World
Hello World

برگرداندن مقدار از توابع دکوراتور 

حالا اگر بخواهیم مقداری را از تابع دکوراتور برگردانیم چه باید بکنیم؟ بگذارید ببینیم تابع دکوراتور خودش چه تصمیمی می‌گیرد. مثال ساده زیر را در نظر بگیرید: 

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

حالا سعی کنیم از آن استفاده کنیم: 

hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None

به نظر می‌رسد با این روش نتوانیم خروجی تابع را ببینیم. به این علت که تابع do_twice_wrapper به طور صریح مقداری برنمی‌گرداند، پس فراخوانی تابع  return_greeting(“Adam”) مقدار None را برمی‌گرداند.

برای حل این مشکل، باید کاری کنیم که تابع wrapper خروجی تابع دکوراتور را بازگرداند. پس فایل decorator.py را به صورت زیر تغییر می‌دهیم:

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

و خواهیم داشت:

>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

هویت واقعی دکوراتورها 

یکی از مواردی که کار با پایتون را راحت‌تر می‌کند، قابلیت درون نگری آن است. درون نگری (introspection) در پایتون به این معنی است که اشیا از ویژگی‌های خود اطلاع دارند. به عنوان مثال،‌ یک تابع اسم و اسناد (documentation) مربوط به خودش را می‌داند:

>>> print
<built-in function print>

>>> print.__name__
'print'

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

این قابلیت برای توابعی که خودمان می‌نویسیم هم به همین صورت عمل می‌کند: 

>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:
wrapper_do_twice()

اما تابع say_whee بعد از اینکه در یک دکوراتور مورد استفاده قرار گرفته، درمورد هویتش دچار اشتباه شده است! حالا گزارش می‌دهد که تابع درونی wrapper_do_twice دکوراتور do_twice است. با اینکه این موضوع از نظر فنی درست است، اما اطلاعاتی مفیدی به کاربر نمی‌دهد. 

برای حل این مساله، دکوراتورها باید از functools.wraps@ استفاده کنند، که اطلاعات اصلی تابع را حفظ می‌کند. بیایید دوباره فایل decorators.py  را به روز کنیم‌:

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

نیازی نیست چیزی را درمورد تابع say_whee تغییر دهید و حالا نتیجه درون نگری به صورت زیر خواهد بود:‌

>>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:
say_whee()

حالا تابع say_whee حتی بعد از استفاده در دکوراتور، باز هم هویت خودش را حفظ کرده است. 

چند مثال واقعی از دکوراتورها 

حالا بیایید نگاهی به چند مثال مفیدتر از دکوراتورها بیندازیم. توجه کنید که دکوراتورها به طور کلی از همان الگویی که تا اینجا یاد گرفتیم پیروی می‌کنند: 

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

این فرمول یک تمپلیت boilerplate برای دکوراتورهای پیچیده‌تر است.

نکته: در مثال‌های بعدی فرض می‌کنیم که این دکوراتورها در فایلی به اسم decorators.py ذخیره شده‌اند.

توابع زمان سنج

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

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

این دکوراتور به این صورت کار می‌کند که زمان را دقیقا قبل از شروع اجرای تابع (جایی که با 1# مشخص شده است) و دقیقا بعد از اتمام اجرای آن اندازه گیری می‌کند (در 2#). و مدت زمان اجرای تابع، اختلاف این دو است (یعنی 3#). در این دکوراتور از تابع ()time.perf_counter استفاده کرده‌ایم که برای اندازه گیری بازه‌های زمانی گزینه مناسبی است. حالا بیایید تابع را برای مقادیر مختلف امتحان کنیم:‌

>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs

پیشنهاد می‌کنیم، خودتان سعی کنید کد را اجرا کنید و آن را خط به خط بررسی کنید و مطمئن شوید که درک مناسبی از آن‌ها پیدا کرده‌اید. هرچند، اگر به طور کامل متوجه عملکرد آن‌ها نشدید نگران نشوید، دکوراتورها مباحث پیشرفته‌ای در پایتون به حساب می‌آیند. 

دکوراتور دیباگ کردن کد 

دکوراتور debug@، آرگومان‌های یک تابع و خروجی آن را در هر بار فراخوانی، چاپ می کند:

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

در این دکوراتور مراحل زیر به ترتیب انجام می‌شود: 
۱. لیستی از آرگومان‌های ترتیبی (positional argument) ایجاد می‌کند. از ()repr استفاده می‌کند تا یک رشته مرتب از هر آرگومان ارائه دهد. 
۲. لیستی از آرگومان‌های کیوردی  keyword arguments ایجاد می‌کند. F-string تمام آرگومان‌ها را به شکل  key=value درمی‌آورد. 
۳. تمام آرگومان‌های ترتیبی و کیوردی به هم متصل می‌شوند و یک رشته signature می‌سازند، به طوری که بین هر آرگومان یک کاما قرار گیرد. 
۴. مقدار بازگشتی بعد از اینکه تابع اجرا شد، چاپ می‌شود.

بیایید با اعمال این دکوراتور بر روی یک تابع ساده، ببینیم این دکوراتور در عمل چطور کار می‌کند. این تابع یک آرگومان ترتیبی و یک آرگومان کیورد دارد:‌

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"

توجه کنید که دکوراتور debug@ چطور signature را چاپ می‌کند و مقدار تابع ()make_greeting را باز می‌گرداند:

>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'

>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'

ممکن است با این مثال فکر کنید که دکوراتور debug@ چندان هم مفید نیست، چون فقط دقیقا همان چیزی را که خودمان نوشته‌ایم چاپ می‌کند. اما این دکوراتور وقتی قدرتمندتر می‌شود که به توابع متداول کوچکی که می‌شناسیم و خودمان آن‌ها را ننوشته‌ایم، اعمال شود.

مثال زیر مقدار عدد ریاضی ثابت e را تخمین می‌زند: 

import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

این مثال همچنین نشان می‌دهد که می‌توانیم دکوراتور را، برای تابعی که از قبل نوشته شده است به کار ببریم. 

وقتی تابع ()approximate_e را فراخوانی می‌کنیم، می‌توانیم ببینیم که دکوراتور  debug@ چگونه کار می‌کند: 

>>> approximate_e(5)
Calling factorial(0)
'factorial' returned 1
Calling factorial(1)
'factorial' returned 1
Calling factorial(2)
'factorial' returned 2
Calling factorial(3)
'factorial' returned 6
Calling factorial(4)
'factorial' returned 24
2.708333333333333

در این مثال دیدیم که فقط با ۵ جمله، می‌توانیم تقریب خوبی از عدد e به دست بیاوریم. 

 

کاربردها و مثال‌های دکوراتورها به همین جا ختم نمی‌شود. کلاس‌های دکوراتورها، دکوراتورهای تو در تو و دکوراتورهایی با آرگومان، مواردی هستند که توصیه می‌کنیم نگاهی به آن‌ها هم بیندازید. 

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