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 رفتار کلاسها را تغییر میدهند