~/icsd.ir — bash
SYSTEM_ONLINE

Decorators – دکوراتورها در عمق

دکوراتورها (Decorators) یکی از قدرتمندترین قابلیت‌های پایتون هستند که به ما اجازه می‌دهند رفتار توابع یا کلاس‌ها را بدون تغییر کد آن‌ها گسترش دهیم. در این فصل با Function Decorators، Class Decorators، Decorators پارامتری و الگوهای رایج آشنا می‌شویم.

دکوراتورها (Decorators) یکی از قدرتمندترین قابلیت‌های پایتون هستند که به ما اجازه می‌دهند رفتار توابع یا کلاس‌ها را بدون تغییر کد آن‌ها گسترش دهیم. در این فصل با Function Decorators، Class Decorators، Decorators پارامتری و الگوهای رایج آشنا می‌شویم.

دکوراتور چیست؟

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

def my_decorator(func):
    def wrapper():
        print("قبل از اجرا")
        func()
        print("بعد از اجرا")
    return wrapper

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

say_hello()
# قبل از اجرا
# سلام!
# بعد از اجرا

سینتکس @my_decorator معادل این است:

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

say_hello = my_decorator(say_hello)

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

برای پشتیبانی از توابع با هر ورودی، از *args, **kwargs استفاده می‌کنیم:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"فراخوانی {func.__name__} با args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"نتیجه: {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def greet(name, greeting="سلام"):
    return f"{greeting} {name}"

add(3, 5)
# فراخوانی add با args=(3, 5), kwargs={}
# نتیجه: 8

greet("علی", greeting="درود")
# فراخوانی greet با args=('علی',), kwargs={'greeting': 'درود'}
# نتیجه: درود علی

functools.wraps – حفظ متادیتا

بدون @wraps، نام و docstring تابع اصلی از بین می‌رود:

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def my_func():
    """این داک‌استرینگ مهم است"""
    pass

print(my_func.__name__)  # wrapper (نه my_func!)
print(my_func.__doc__)   # None

راه‌حل: استفاده از functools.wraps:

from functools import wraps

def good_decorator(func):
    @wraps(func)  # متادیتای func را به wrapper منتقل می‌کند
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def my_func():
    """این داک‌استرینگ مهم است"""
    pass

print(my_func.__name__)  # my_func ✓
print(my_func.__doc__)   # این داک‌استرینگ مهم است ✓
قاعده طلایی: همیشه از @wraps در دکوراتورهایتان استفاده کنید.

دکوراتورهای پارامتری

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

from functools import wraps

def repeat(times):
    """دکوراتور با پارامتر - تابع را N بار تکرار می‌کند"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"سلام {name}")

greet("علی")
# سلام علی
# سلام علی
# سلام علی

دکوراتور با پارامتر اختیاری

الگوی پیشرفته‌ای که هم با و هم بدون پرانتز کار می‌کند:

from functools import wraps

def log(func=None, *, level="INFO"):
    if func is None:
        # @log(level="DEBUG")
        return lambda f: log(f, level=level)
    
    # @log
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[{level}] {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log
def func1():
    pass

@log(level="DEBUG")
def func2():
    pass

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

۱. Timer – اندازه‌گیری زمان اجرا

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} در {elapsed:.4f} ثانیه اجرا شد")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()  # slow_function در 1.0012 ثانیه اجرا شد

۲. Retry – تلاش مجدد در صورت خطا

import time
from functools import wraps

def retry(times=3, delay=1, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    print(f"تلاش {attempt+1}/{times} ناموفق: {e}")
                    if attempt < times - 1:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(times=3, delay=2, exceptions=(ConnectionError,))
def fetch_data(url):
    import requests
    return requests.get(url).json()

۳. Cache (Memoization)

from functools import wraps

def cache(func):
    """کش ساده برای توابع pure"""
    storage = {}
    @wraps(func)
    def wrapper(*args):
        if args not in storage:
            storage[args] = func(*args)
        return storage[args]
    wrapper.cache_clear = storage.clear
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(50))  # سریع - بدون کش بسیار کند می‌بود

پایتون خودش functools.lru_cache را دارد:

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci.cache_info())
# CacheInfo(hits=0, misses=51, maxsize=128, currsize=51)

۴. Validate – اعتبارسنجی ورودی

from functools import wraps

def validate_types(**type_specs):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # ترکیب args و kwargs
            import inspect
            sig = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            
            for param, expected_type in type_specs.items():
                if param in bound.arguments:
                    value = bound.arguments[param]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"{param} باید از نوع {expected_type.__name__} باشد"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def register(name, age):
    return f"{name} ({age} ساله)"

register("علی", 30)       # OK
# register("علی", "30")   # TypeError

@property – دسترسی کنترل‌شده

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """خواندن"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """نوشتن - با اعتبارسنجی"""
        if value < -273.15:
            raise ValueError("دما نمی‌تواند زیر صفر مطلق باشد")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """فقط خواندنی - محاسبه‌شده"""
        return self._celsius * 9/5 + 32
    
    @celsius.deleter
    def celsius(self):
        print("حذف دما")
        del self._celsius

t = Temperature(25)
print(t.celsius)        # 25
print(t.fahrenheit)     # 77.0
t.celsius = 30          # setter
# t.celsius = -300      # ValueError
# t.fahrenheit = 100    # AttributeError - فقط خواندنی

Class Decorators

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

def add_repr(cls):
    """به هر کلاسی __repr__ اضافه می‌کند"""
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

u = User("علی", "ali@example.com")
print(u)  # User(name='علی', email='ali@example.com')

Singleton با Class Decorator

def singleton(cls):
    """فقط یک instance از کلاس مجاز است"""
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("اتصال به دیتابیس")

db1 = Database()  # اتصال به دیتابیس
db2 = Database()  # (پیغامی چاپ نمی‌شود)
print(db1 is db2)  # True

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

وقتی چند دکوراتور روی هم می‌گذارید، از پایین به بالا اعمال می‌شوند:

@decorator_a   # 3) آخرین لایه
@decorator_b   # 2) لایه دوم
@decorator_c   # 1) اولین لایه روی تابع اصلی
def func():
    pass

# معادل: func = decorator_a(decorator_b(decorator_c(func)))

بهترین شیوه‌ها

  • همیشه از @functools.wraps استفاده کنید
  • دکوراتور باید تابعی با همان signature برگرداند
  • برای پارامتر، یک لایه تو در تو اضافه کنید
  • از functools.lru_cache به‌جای cache دستی استفاده کنید
  • استثناءها را در دکوراتور log کنید، اما raise مجدد کنید

جمع‌بندی

  • دکوراتور تابعی است که تابع دیگری را بسط می‌دهد
  • *args, **kwargs برای پشتیبانی از هر signature
  • @wraps برای حفظ متادیتا ضروری است
  • دکوراتورهای پارامتری نیاز به یک لایه تو در توی اضافه دارند
  • کاربردهای رایج: timing، retry، caching، validation، logging
  • Class decorators رفتار کلاس‌ها را تغییر می‌دهند

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.