دک

دکوراتورها در Python

{one: "۱ تمرین", other: "%{count} تمرین"}

درباره‌ی دکوراتورها

دکوراتورها توابعی هستند که تابعی دیگر را به عنوان آرگومان می‌گیرند، با هدف گسترش یا جایگزینی رفتار تابع ورودی. اگر تابع A یک دکوراتور باشد و تابع B آرگومان آن، آنگاه تابع A رفتار تابع B را تغییر می‌دهد، گسترش می‌دهد یا جایگزین می‌کند، بدون اینکه کد تابع B را تغییر دهد. می‌گوییم که تابع دکوراتور A تابع B را می‌پوشاند. هرچند از «تغییر» رفتار حرف می‌زنیم، تابع پوشانده‌شده در واقع تغییر نمی‌کند. رفتار یا پیرامون تابع پوشانده‌شده (و آنچه برمی‌گرداند) اضافه می‌شود، یا رفتار تابع پوشانده‌شده با رفتاری دیگر جانشین می‌شود.

دکوراتور یک تابع مرتبه‌ی بالا است

یک تابع مرتبه‌ی بالا تابعی است که یک یا چند تابع را به عنوان آرگومان می‌پذیرد و/یا یک یا چند تابع برمی‌گرداند. تابعی که به عنوان آرگومان به کار می‌رود یا از تابعی دیگر برگردانده می‌شود، یک تابع درجه‌یک است. یک تابع پایتون، به عنوان یک شیء قابل فراخوانی، تابعی درجه‌یک است که می‌توان آن را در متغیری ذخیره کرد یا مانند هر مقدار یا شیء دیگری به عنوان آرگومان به کار برد. توابع مرتبه‌ی بالا و توابع درجه‌یک با هم کار می‌کنند تا دکوراتورها ممکن شوند.

استفاده از یک دکوراتور چه شکلی است

نماد @ به ابتدای اسم تابع دکوراتور می‌آید و درست بالای تابعی قرار می‌گیرد که باید دکوراته شود، به این شکل:

@decorator
def decorated_function():
    pass

برخی دکوراتورها آرگومان می‌پذیرند:

@decorator_with_arg(name="Bob")
def decorated_function2():
    pass

اگر دکوراتوری آرگومان‌های پیش‌فرض تعریف کرده باشد، برای اینکه دکوراتور کار کند باید در فراخوانی @decorator() پرانتز بگذارید، همان‌طور که در فراخوانی هر تابعی این کار را می‌کنید:

@decorator_with_default_arg()
def decorated_function3():
    pass

اگر دکوراتوری آرگومان‌های مکانی بگیرد، ندادن آن آرگومان‌ها به خطایی منجر می‌شود که چیزی شبیه این است:

TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'

نحوه‌ی نگارش @decorator یک «شکر نحوی» یا شکل کوتاه‌شده‌ای برای فراخوانی تابع دکوراتور و فرستادن تابع دکوراته‌شده به آن به عنوان آرگومان است. در ادامه نمونه‌هایی از روش‌های جایگزین برای فراخوانی یک دکوراتور می‌آید:

def function():
    pass

function = decorator(function)


def function2():
    pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
    pass

function3 = decorator_with_default_arg()(function3)

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

بیشتر دکوراتورها برای گسترش یا جایگزینی رفتار تابعی دیگر در نظر گرفته شده‌اند، اما برخی دکوراتورها ممکن است کاری نکنند جز برگرداندن تابعی که می‌پوشانند.

دکوراتورها توابعی هستند که دست‌کم یک آرگومان می‌گیرند: تابعی که آن را می‌پوشانند. آن‌ها معمولاً یا تابع پوشانده‌شده را برمی‌گردانند یا نتیجه‌ی عبارتی را که از تابع پوشانده‌شده استفاده می‌کند.

یک دکوراتور ساده، یعنی یکی که تنها تابع پوشانده‌شده‌اش را برمی‌گرداند، می‌تواند به این شکل نوشته شود:

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

یک دکوراتور ممکن است فقط عوارض جانبی اضافه کند، مانند اطلاعات بیشتری که برای ثبت وقایع به کار می‌رود:

>>> def my_logger(func):
...     print(f"Entering {func.__name__}")
...     return func
...
... @my_logger
... def my_func():
...     print("Hello")
...
>>> my_func()
Entering my_func
Hello

دکوراتور خودش را برنمی‌گرداند. ممکن است آرگومان‌های تابعش، تابعی دیگر، یا یک یا چند مقدار را برگرداند که جایگزین مقدار بازگشتی تابع ورودی یا دکوراته‌شده می‌شوند. اگر دکوراتوری تابعی دیگر برگرداند، معمولاً یک تابع درونی برمی‌گرداند.

توابع درونی

یک تابع می‌تواند درون تابعی دیگر تعریف شود. به چنین تابع تودرتویی تابع درونی می‌گویند. یک دکوراتور ممکن است از یک تابع درونی برای پوشاندن آرگومان تابعش استفاده کند. سپس دکوراتور تابع درونی‌اش را برمی‌گرداند. تابع درونی ممکن است آرگومان تابع اصلی را برگرداند.

یک دکوراتور اعتبارسنج با استفاده از یک تابع درونی

در ادامه نمونه‌ای از دکوراتوری می‌آید که برای اعتبارسنجی به کار می‌رود:

def my_validator(func):
    def my_wrapper(world):
        print(f"Entering {func.__name__} with {world} argument")
        
        if world == "Pluto":
            print("Pluto is not a planet!")
        else:
            return func(world)
    return my_wrapper

@my_validator
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!

در خط اول، تعریف آرگومان func دکوراتور را داریم. در خط بعدی تعریف تابع درونی دکوراتور قرار دارد که آرگومان func را می‌پوشاند. چون تابع درونی آرگومان func دکوراتور را می‌پوشاند، همان آرگومانی به آن داده می‌شود که به func داده می‌شود. توجه کنید که پوشاننده لازم نیست برای آرگومان همان اسمی را به کار ببرد که در func تعریف شده است. تابع اصلی از planet استفاده می‌کند و دکوراتور از world، و دکوراتور همچنان کار می‌کند.

تابع درونی یا func را برمی‌گرداند، یا اگر world == "Pluto" باشد، چاپ می‌کند که پلوتو یک سیاره نیست. می‌توان طوری نوشت که به‌جایش ValueError ایجاد کند. پس تابع درونی func را می‌پوشاند و یا func را برمی‌گرداند، یا کاری می‌کند که جانشین کاری می‌شود که func انجام می‌داد. دکوراتور _تابع درونی_اش را برمی‌گرداند. تابع درونی ممکن است تابع اصلی ورودی را برگرداند یا نبرد. بسته به اینکه چه کدی به‌صورت شرطی در تابع پوشاننده یا تابع درونی اجرا شود، ممکن است func برگردانده شود، خطایی ایجاد شود، یا مقداری از نوع برگشتی func بازگردانده شود.

دکوراته کردن تابعی که تعداد دلخواهی آرگومان می‌گیرد

می‌توان دکوراتورها را برای توابعی نوشت که تعداد دلخواهی آرگومان می‌گیرند، با استفاده از نحوه‌ی نگارش *args و **kwargs.

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

def double(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

@double
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 18

print(add(2, 3, 4, 5, 6))
#-> 40

این برای دو برابر کردن مقدار برگشتی آرگومان تابع کار می‌کند. اگر بخواهیم مقدار برگشتی را سه برابر، چهار برابر و غیره کنیم، می‌توانیم پارامتری به خود دکوراتور اضافه کنیم، همان‌طور که در بخش بعدی نشان می‌دهیم.

دکوراتورهایی که پارامترهای خودشان را دارند

در ادامه نمونه‌ای از دکوراتوری می‌آید که می‌توان آن را طوری تنظیم کرد که مقدار برگشتی تابع دکوراته‌شده را در مقداری دلخواه ضرب کند:

def multi(factor=1):
     if factor == 0:
        raise ValueError("factor must not be 0")

     def outer_wrapper(func):
         def inner_wrapper(*args, **kwargs):
             return func(*args, **kwargs) * factor
         return inner_wrapper
     return outer_wrapper

@multi(factor=3)
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 27

print(add(2, 3, 4, 5, 6))
#-> 60

خطوط اول بررسی می‌کنند که factor برابر 0 نباشد. سپس پوشاننده‌ی بیرونی تعریف می‌شود. این همان امضایی را دارد که برای یک دکوراتور بدون پارامتر انتظار داریم. پوشاننده‌ی بیرونی تابعی درونی با همان امضای تابع اصلی دارد. پوشاننده‌ی درونی کار ضرب مقدار برگشتی تابع اصلی در آرگومان داده‌شده به دکوراتور را انجام می‌دهد. پوشاننده‌ی بیرونی پوشاننده‌ی درونی را برمی‌گرداند و دکوراتور پوشاننده‌ی بیرونی را برمی‌گرداند.

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

def check_for_pluto(check=True):
    def my_validator(func):
        def my_wrapper(world):
            print(f"Entering {func.__name__} with {world} argument")
            if check and world == "Pluto":
                print("Pluto is not a planet!")
            else:
                return func(world)
        
        return my_wrapper
    return my_validator

@check_for_pluto(check=False)
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!

این امکان می‌دهد به‌راحتی بین بررسی Pluto و بررسی‌نکردن آن جابه‌جا شوید، و این کار بدون نیاز به تغییر my_func انجام می‌شود.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود