دکوریتورها در پایتون با مثال

مقدمه

دکوریتورها (Decorators) یکی از ویژگی‌های قدرتمند و در عین حال ظریف زبان برنامه‌نویسی پایتون هستند. آنها به شما اجازه می‌دهند تا رفتار توابع یا متدها را به شیوه‌ای تمیز و قابل خواندن تغییر داده یا گسترش دهید، بدون اینکه کد اصلی تابع را مستقیماً ویرایش کنید. این قابلیت به خصوص برای افزودن کارکردهای جانبی (crosscutting concerns) مانند لاگ‌گیری (logging)، کنترل دسترسی، بررسی زمان اجرا، و غیره بسیار مفید است.

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

  1. توابع به عنوان اشیاء درجه اول (FirstClass Objects): در پایتون، توابع مانند سایر اشیاء (مثل اعداد، رشته‌ها، لیست‌ها) هستند. این به این معنی است که می‌توان آنها را به متغیرها اختصاص داد، به عنوان آرگومان به توابع دیگر ارسال کرد و حتی از درون توابع دیگر برگرداند.
  2. توابع تو در تو (Nested Functions): می‌توان توابعی را درون توابع دیگر تعریف کرد. تابع داخلی فقط در محدوده (scope) تابع بیرونی قابل دسترس است.
  3. کلوژرها (Closures): تابع داخلی می‌تواند به متغیرهای تابع بیرونی خود دسترسی داشته باشد، حتی پس از اینکه اجرای تابع بیرونی به پایان رسیده باشد.

دکوریتورها از این مفاهیم بهره می‌برند تا بتوانند یک تابع را “بپوشانند” (wrap) و قبل یا بعد از اجرای آن، کدهای اضافی را اجرا کنند.

ساختار پایه یک دکوریتور

یک دکوریتور در ساده‌ترین شکل خود، یک تابع است که تابع دیگری را به عنوان ورودی می‌گیرد، یک تابع جدید (معمولاً یک تابع داخلی به نام wrapper) تعریف می‌کند که رفتار تابع اصلی را به نحوی تغییر می‌دهد و در نهایت، این تابع جدید را برمی‌گرداند.

def my_decorator(func):
    def wrapper():
        print("اتفاقی قبل از اجرای تابع اصلی رخ می‌دهد.")
        func()  # اجرای تابع اصلی
        print("اتفاقی بعد از اجرای تابع اصلی رخ می‌دهد.")
    return wrapper

def say_hello():
    print("سلام!")

# روش کلاسیک اعمال دکوریتور
decorated_hello = my_decorator(say_hello)
decorated_hello()

خروجی:

اتفاقی قبل از اجرای تابع اصلی رخ می‌دهد.
سلام!
اتفاقی بعد از اجرای تابع اصلی رخ می‌دهد.

استفاده از سینتکس @

پایتون یک سینتکس ساده‌تر و خواناتر برای اعمال دکوریتورها با استفاده از علامت @ فراهم می‌کند. این سینتکس معادل همان فراخوانی my_function = my_decorator(my_function) است.

def my_decorator(func):
    def wrapper():
        print("اتفاقی قبل از اجرای تابع اصلی رخ می‌دهد.")
        func()
        print("اتفاقی بعد از اجرای تابع اصلی رخ می‌دهد.")
    return wrapper

@my_decorator
def say_hello():
    print("سلام!")

say_hello() # حالا تابع say_hello به طور خودکار دکوریت شده است

خروجی این کد نیز دقیقاً مانند مثال قبلی خواهد بود. استفاده از @ بسیار رایج‌تر و خواناتر است.

مثال 1: دکوریتور لاگ‌گیری ساده

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

import time
import functools # برای استفاده از wraps

def log_execution(func):
    @functools.wraps(func) # حفظ اطلاعات تابع اصلی
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print(f"شروع اجرای تابع: {func.__name__}")
        result = func(*args, **kwargs) # اجرای تابع اصلی با آرگومان‌هایش
        end_time = time.time()
        print(f"پایان اجرای تابع: {func.__name__}، زمان اجرا: {end_time - start_time:.4f} ثانیه")
        return result
    return wrapper

@log_execution
def calculate_sum(a, b):
    """این تابع مجموع دو عدد را محاسبه می‌کند."""
    time.sleep(1) # شبیه‌سازی عملیات زمان‌بر
    return a + b

@log_execution
def greet(name):
    """این تابع یک پیام خوشامدگویی نمایش می‌دهد."""
    print(f"سلام، {name}!")

# فراخوانی توابع دکوریت شده
sum_result = calculate_sum(5, 3)
print(f"نتیجه جمع: {sum_result}")

greet("کاربر")

print(f"نام تابع calculate_sum: {calculate_sum.__name__}")
print(f"داک‌استرینگ تابع calculate_sum: {calculate_sum.__doc__}")

خروجی:

شروع اجرای تابع: calculate_sum
پایان اجرای تابع: calculate_sum، زمان اجرا: 1.00xx ثانیه
نتیجه جمع: 8
شروع اجرای تابع: greet
سلام، کاربر!
پایان اجرای تابع: greet، زمان اجرا: 0.000x ثانیه
نام تابع calculate_sum: calculate_sum
داک‌استرینگ تابع calculate_sum: این تابع مجموع دو عدد را محاسبه می‌کند.

نکات مهم در مثال بالا:

  1. *args و **kwargs: تابع wrapper از *args و **kwargs استفاده می‌کند تا بتواند هر تعداد آرگومان موقعیتی و کلیدواژه‌ای را از تابع دکوریت شده دریافت و به آن ارسال کند. این باعث می‌شود دکوریتور ما عمومی باشد و بتواند هر تابعی با هر امضایی (signature) را دکوریت کند.
  2. @functools.wraps(func): این یک دکوریتور دیگر است که بر روی تابع wrapper اعمال می‌شود. وظیفه آن کپی کردن اطلاعات مهم تابع اصلی (مانند نام __name__، داک‌استرینگ __doc__ و غیره) به تابع wrapper است. بدون استفاده از @wraps، اگر سعی کنید به calculate_sum.__name__ یا calculate_sum.__doc__ دسترسی پیدا کنید، اطلاعات تابع wrapper را دریافت خواهید کرد که معمولاً مطلوب نیست.

مثال 2: دکوریتور کنترل دسترسی ساده

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

import functools

# فرض کنید یک دیکشنری برای نقش‌های کاربری داریم
USER_ROLES = {
    "admin": True,
    "guest": False
}

def requires_admin(func):
    @functools.wraps(func)
    def wrapper(user_role, *args, **kwargs):
        if USER_ROLES.get(user_role, False):
            print(f"کاربر با نقش '{user_role}' مجاز است.")
            return func(user_role, *args, **kwargs)
        else:
            print(f"خطا: کاربر با نقش '{user_role}' مجاز به اجرای {func.__name__} نیست.")
            # در یک برنامه واقعی، می‌توان یک Exception ایجاد کرد
            return None
    return wrapper

@requires_admin
def delete_user(user_role, username):
    """فقط ادمین می‌تواند کاربران را حذف کند."""
    print(f"کاربر '{username}' حذف شد.")

@requires_admin
def view_dashboard(user_role):
    """فقط ادمین می‌تواند داشبورد را ببیند."""
    print("نمایش داشبورد...")

# فراخوانی با نقش ادمین
delete_user("admin", "test_user")
view_dashboard("admin")

print("-" * 20)

# فراخوانی با نقش مهمان
delete_user("guest", "another_user")
view_dashboard("guest")

# فراخوانی با نقش نامعتبر
delete_user("editor", "some_user")

خروجی:

کاربر با نقش 'admin' مجاز است.
کاربر 'test_user' حذف شد.
کاربر با نقش 'admin' مجاز است.
نمایش داشبورد...
--------------------
خطا: کاربر با نقش 'guest' مجاز به اجرای delete_user نیست.
خطا: کاربر با نقش 'guest' مجاز به اجرای view_dashboard نیست.
خطا: کاربر با نقش 'editor' مجاز به اجرای delete_user نیست.

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

گاهی اوقات لازم است دکوریتور شما خودش پارامترهایی را بپذیرد تا رفتار آن قابل تنظیم باشد. برای مثال، ممکن است بخواهید حداکثر تعداد تلاش مجدد برای اجرای یک تابع را مشخص کنید. برای این کار، باید یک لایه تابع اضافی ایجاد کنیم. تابع بیرونی آرگومان‌های دکوریتور را می‌گیرد و یک تابع دکوریتور (مانند آنچه قبلاً دیدیم) را برمی‌گرداند.

import functools
import time

def retry(max_attempts):
    def decorator_retry(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    print(f"[تلاش {attempts + 1}/{max_attempts}] اجرای تابع {func.__name__}...")
                    result = func(*args, **kwargs)
                    print("اجرا موفقیت آمیز بود.")
                    return result
                except Exception as e:
                    attempts += 1
                    print(f"خطا در اجرای تابع {func.__name__}: {e}")
                    if attempts >= max_attempts:
                        print("تعداد تلاش‌ها به حداکثر رسید. عملیات ناموفق بود.")
                        raise # یا مقدار None برگردانید یا خطا را مجددا ایجاد کنید
                    else:
                        print("تلاش مجدد...")
                        time.sleep(1) # تاخیر قبل از تلاش مجدد
        return wrapper
    return decorator_retry

# استفاده از دکوریتور با آرگومان
@retry(max_attempts=3)
def connect_to_service(service_name):
    """شبیه‌سازی اتصال به یک سرویس که ممکن است شکست بخورد."""
    import random
    if random.random() < 0.7: # 70% احتمال شکست
        raise ConnectionError(f"اتصال به {service_name} برقرار نشد.")
    else:
        print(f"اتصال به {service_name} موفقیت آمیز بود.")
        return True

# فراخوانی تابع
connect_to_service("Database")
print("-" * 20)
connect_to_service("API")

خروجی احتمالی (بسته به اعداد تصادفی):

[تلاش 1/3] اجرای تابع connect_to_service...
خطا در اجرای تابع connect_to_service: اتصال به Database برقرار نشد.
تلاش مجدد...
[تلاش 2/3] اجرای تابع connect_to_service...
خطا در اجرای تابع connect_to_service: اتصال به Database برقرار نشد.
تلاش مجدد...
[تلاش 3/3] اجرای تابع connect_to_service...
اتصال به Database موفقیت آمیز بود.
اجرا موفقیت آمیز بود.
--------------------
[تلاش 1/3] اجرای تابع connect_to_service...
خطا در اجرای تابع connect_to_service: اتصال به API برقرار نشد.
تلاش مجدد...
[تلاش 2/3] اجرای تابع connect_to_service...
اتصال به API موفقیت آمیز بود.
اجرا موفقیت آمیز بود.

زنجیره‌سازی دکوریتورها (Chaining Decorators)

می‌توان چندین دکوریتور را به یک تابع اعمال کرد. ترتیب اعمال آنها مهم است: دکوریتورها از پایین به بالا (نزدیک‌ترین به تابع) اعمال می‌شوند.

import functools

def decorator_one(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("ورود به دکوریتور یک")
        result = func(*args, **kwargs)
        print("خروج از دکوریتور یک")
        return result
    return wrapper

def decorator_two(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("ورود به دکوریتور دو")
        result = func(*args, **kwargs)
        print("خروج از دکوریتور دو")
        return result
    return wrapper

@decorator_one
@decorator_two
def my_function():
    print("اجرای تابع اصلی")

my_function()

خروجی:

ورود به دکوریتور یک
ورود به دکوریتور دو
اجرای تابع اصلی
خروج از دکوریتور دو
خروج از دکوریتور یک

همانطور که می‌بینید، decorator_one تابع دکوریت شده توسط decorator_two را می‌پوشاند. بنابراین، decorator_one بیرونی‌ترین لایه است و اولین ورودی و آخرین خروجی را چاپ می‌کند.

دکوریتورهای کلاس (Class Decorators)

علاوه بر توابع، می‌توان از کلاس‌ها نیز برای ایجاد دکوریتورها استفاده کرد. یک کلاس دکوریتور معمولاً متد __init__ را برای دریافت تابع اصلی و متد __call__ را برای پیاده‌سازی منطق پوشش‌دهنده (wrapper logic) تعریف می‌کند.

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func) # مشابه wraps برای کلاس
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"فراخوانی شماره {self.num_calls} برای تابع {self.func.__name__}")
        return self.func(*args, **kwargs)

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

say_whee()
say_whee()
say_whee()

print(f"نام تابع: {say_whee.__name__}") # به لطف update_wrapper

خروجی:

فراخوانی شماره 1 برای تابع say_whee
Whee!
فراخوانی شماره 2 برای تابع say_whee
Whee!
فراخوانی شماره 3 برای تابع say_whee
Whee!
نام تابع: say_whee

دکوریتورهای داخلی پایتون

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

  • @staticmethod: متدی را تعریف می‌کند که به نمونه‌ای از کلاس (instance) یا خود کلاس به عنوان اولین آرگومان ضمنی (مانند self یا cls) دسترسی ندارد. مانند یک تابع معمولی است که درون فضای نام کلاس قرار گرفته است.
  • @classmethod: متدی را تعریف می‌کند که خود کلاس را به عنوان اولین آرگومان ضمنی (معمولاً به نام cls) دریافت می‌کند. برای ایجاد متدهایی که بر روی خود کلاس عمل می‌کنند (مانند factory methods) مفید است.
  • @property: متدی را به یک ویژگی فقط خواندنی (readonly property) تبدیل می‌کند. این امکان را می‌دهد که به یک متد مانند یک attribute دسترسی داشته باشید، بدون نیاز به پرانتز فراخوانی (). اغلب با @property_name.setter و @property_name.deleter برای کنترل نحوه تنظیم و حذف مقدار استفاده می‌شود.
class MyClass:
    def __init__(self, value):
        self._value = value # ویژگی "خصوصی"

    def regular_method(self):
        print(f"مقدار نمونه: {self._value}")

    @staticmethod
    def utility_method():
        print("این یک متد استاتیک است.")

    @classmethod
    def class_factory(cls, value):
        print(f"ایجاد نمونه از کلاس {cls.__name__} با مقدار {value}")
        return cls(value * 2) # یک نمونه جدید با مقدار تغییر یافته می‌سازد

    @property
    def value(self):
        """داک استرینگ برای پراپرتی value"""
        print("دسترسی به مقدار از طریق پراپرتی...")
        return self._value

    @value.setter
    def value(self, new_value):
        print("تنظیم مقدار جدید از طریق ستر...")
        if new_value >= 0:
            self._value = new_value
        else:
            print("خطا: مقدار نمی‌تواند منفی باشد.")

# استفاده از متد استاتیک
MyClass.utility_method()

# استفاده از متد کلاس
obj1 = MyClass.class_factory(10) # obj1._value خواهد بود 20

# استفاده از پراپرتی
obj2 = MyClass(5)
print(obj2.value) # دسترسی مانند attribute

obj2.value = 15 # استفاده از ستر
print(obj2.value)

obj2.value = -5 # تلاش برای تنظیم مقدار نامعتبر
print(obj2.value) # مقدار تغییر نکرده است

نتیجه‌گیری

دکوریتورها یک ابزار بسیار قدرتمند در پایتون هستند که به شما امکان می‌دهند قابلیت‌های جدیدی به توابع و متدها اضافه کنید بدون اینکه نیاز به تغییر کد اصلی آنها باشد. این منجر به کدی تمیزتر، خواناتر و با قابلیت استفاده مجدد بالاتر می‌شود. با درک مفاهیم توابع درجه اول، توابع تودرتو و کلوژرها، و با استفاده هوشمندانه از سینتکس @ و ماژول functools، می‌توانید دکوریتورهای سفارشی خود را برای حل طیف وسیعی از مسائل برنامه‌نویسی ایجاد کنید، از لاگ‌گیری و زمان‌سنجی گرفته تا کنترل دسترسی و اعتبارسنجی.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *