Kodax

Decorators — Python المتقدم

المزخرفات (Decorators)

المزخرف دالة تأخذ دالة وترجع نسخة معدّلة منها. تستخدمها لإضافة سلوك مشترك (قياس الوقت، التسجيل، التحقق من الصلاحيات، التخزين المؤقت) لعدة دوال دون تكرار الكود داخل كل منها. يعتمد على ما تعلمته: الدوال كقيم والإغلاقات.

الفكرة بدون صيغة @

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"استدعاء {func.__name__} بـ {args}")
        return func(*args, **kwargs)
    return wrapper

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

add = log_calls(add)   # استبدلنا add بالنسخة المغلّفة
add(2, 3)

صيغة @ هي اختصار لنفس السطر

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

تعني حرفياً: add = log_calls(add).

لماذا *args و **kwargs؟

لكي يعمل الـ wrapper مع أي دالة مهما كانت معاملاتها: *args تجمع المعاملات الموضعية في tuple، و **kwargs تجمع المعاملات بالاسم في قاموس، ثم نمررها كما هي للدالة الأصلية. ولا تنسَ return نتيجة الدالة الأصلية وإلا سترجع الدالة المزخرفة None.

functools.wraps

بدونها تفقد الدالة اسمها وتوثيقها (add.__name__ يصبح "wrapper")، مما يربك رسائل الأخطاء وأدوات الاختبار:

from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
    return wrapper

اجعلها عادة في كل مزخرف.

مزخرف بمعاملات

مستوى إضافي من الدوال: دالة تأخذ الإعدادات وترجع المزخرف:

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def hello():
    print("مرحباً")

مزخرفات جاهزة مفيدة

| المزخرف | الوظيفة |

|---|---|

| @functools.lru_cache | يحفظ نتائج الاستدعاءات السابقة (ممتاز للدوال البطيئة النقية) |

| @property، @classmethod، @staticmethod | رأيتها في دورة OOP |

| @dataclass | يولّد دوال الصنف |

ترتيب عدة مزخرفات

def a(func):
    print("a تُطبَّق ثانياً")
    return func

def b(func):
    print("b تُطبَّق أولاً")
    return func

@a
@b
def f(): ...

تساوي f = a(b(f)): الأقرب للدالة يُطبَّق أولاً.

تمرين

اكتب مزخرف @timer يطبع زمن تنفيذ الدالة، ومزخرف @retry(times) يعيد المحاولة إذا رمت الدالة استثناءً.

# Decorator بسيط
def my_decorator(func):
    def wrapper():
        print("قبل الدالة")
        func()
        print("بعد الدالة")
    return wrapper

@my_decorator
def say_hello():
    print("مرحباً!")

say_hello()
# قبل الدالة
# مرحباً!
# بعد الدالة

# Decorator مع معاملات
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"مرحباً {name}!")

greet("أحمد")

# Timer Decorator
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} استغرق {end - start:.4f} ثانية")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "انتهى!"

print(slow_function())

# @property - جعل method تبدو كـ attribute
class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("لا يمكن أن يكون نصف القطر سالباً")
        self._radius = value
    
    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(circle.radius)  # 5
print(circle.area)    # 78.53975
circle.radius = 10
print(circle.area)    # 314.159
اختبر فهمك