# المزخرفات (decorators) في Python: ما هي وكيف تعمل وكيف تكتب مزخرفاً خاصاً

URL: https://skillaria.top/ar/guides/python/decorators-python

المزخرف في Python دالة تستقبل دالة أخرى وتعيد دالة جديدة، عادةً غلافاً حول الأصلية، يضيف سلوكاً قبل الاستدعاء أو بعده، مثل التسجيل وقياس الوقت والتخزين المؤقت. يُكتب بالرمز @ فوق تعريف الدالة، ويُفضّل أن يستخدم functools.wraps للحفاظ على اسم الدالة وتوثيقها.

المزخرف (decorator) في Python دالة تستقبل دالة أخرى وتعيد دالة جديدة، وغالباً ما تكون غلافاً حول الدالة الأصلية. بهذه الطريقة تُضاف إلى الدالة سلوكيات مثل التسجيل وقياس الوقت والتخزين المؤقت وإعادة المحاولة، دون تغيير كودها. يُكتب المزخرف بالرمز `@اسم` فوق تعريف الدالة، وتجده في كل مكان تقريباً: في Flask وFastAPI وpytest، وفي المكتبة القياسية.

## ما هو المزخرف بكلمات بسيطة

تخيّل هدية وورق التغليف. تبقى الهدية (الدالة) كما هي، لكن تظهر من الخارج طبقة تعمل أولاً. المزخرف هو هذا التغليف: عند الاستدعاء ينفَّذ كود الغلاف أولاً، ثم يستدعي الدالة الأصلية، ويستطيع أن يفعل شيئاً بالنتيجة.

وهذا ممكن لأن الدوال في Python كائنات عادية. يمكن تمريرها إلى دوال أخرى، وإعادتها من الدوال، وتعريفها داخل دوال. وعلى هذه الخصائص الثلاث تقوم المزخرفات.

## كيف يعمل المزخرف: الرمز @ اختصار

لنأخذ مزخرفاً بسيطاً يطبع رسالة قبل الاستدعاء وبعده:

```
def log_calls(func):
    def wrapper():
        print(f"أستدعي {func.__name__}")
        result = func()
        print(f"انتهت {func.__name__}")
        return result
    return wrapper

@log_calls
def hello():
    print("مرحباً!")

hello()
```

```
أستدعي hello
مرحباً!
انتهت hello
```

السطر `@log_calls` فوق `def` يعادل تماماً السطر الذي يأتي بعد التعريف:

```
def hello():
    print("مرحباً!")

hello = log_calls(hello)
```

صار الاسم `hello` يشير إلى `wrapper`، والدالة الأصلية تعيش داخل الإغلاق، فالغلاف يتذكر المتغير `func` من الدالة الخارجية. تفصيل مهم: المزخرف يُطبَّق مرة واحدة، عند تنفيذ `def`، أي عند استيراد الوحدة، أما الغلاف فيعمل مع كل استدعاء.

## الإغلاقات والمزخرفات

الإغلاق (closure) دالة متداخلة تتذكر متغيرات الدالة الخارجية بعد انتهائها. ولولا الإغلاقات لما وُجدت المزخرفات: بهذه الطريقة يصل `wrapper` إلى `func`. ويمكن أن يحفظ الإغلاق حالة، مثل عدّاد للاستدعاءات:

```
def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: الاستدعاء رقم {calls}")
        return func(*args, **kwargs)
    return wrapper
```

## كيف تكتب مزخرفاً في Python: قالب عام

للمثال الأول عيبان: الغلاف لا يقبل وسائط، ويُخفي اسم الدالة الأصلية. والقالب العملي الذي يصلح أن تبدأ به أي مزخرف هو هذا:

```
import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # الكود قبل الاستدعاء
        result = func(*args, **kwargs)
        # الكود بعد الاستدعاء
        return result
    return wrapper
```

ثلاث قواعد في القالب:

1. **`*args, **kwargs`** — يقبل الغلاف أي وسائط ويمررها كما هي.
2. **`return result`** — لا تنسَ إعادة النتيجة، وإلا ستعيد الدالة المزخرفة `None` دائماً.
3. **`@functools.wraps(func)`** — ينقل بيانات الدالة الأصلية الوصفية إلى الغلاف.

مثال على مزخرف يقيس زمن التنفيذ:

```
import functools
import time

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

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # استغرقت slow_sum 0.142 ثانية
```

## لماذا نحتاج functools.wraps

بدون `wraps` يتغير اسم الدالة المزخرفة وتوثيقها وخصائصها الأخرى، فيظهر من الخارج الغلاف `wrapper`:

```
def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """تجمع عددين."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None
```

مع `@functools.wraps(func)` يحصل الغلاف على `__name__` و`__doc__` و`__module__` و`__qualname__` و`__annotations__` من الدالة الأصلية، ويحفظ في الخاصية `__wrapped__` مرجعاً إلى الأصل. وهذا مهم للتصحيح والتتبع، وللأمر `help()`، وللأطر التي تسجّل الدوال بأسمائها، وللدالة `inspect.signature` التي تعرض التوقيع الحقيقي.

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

أحياناً يحتاج المزخرف إلى إعدادات مثل `@retry(times=3)` أو `@app.route("/")`. لذلك يُضاف مستوى إضافي من التداخل: دالة خارجية تستقبل المعاملات وتعيد المزخرف الحقيقي، ويُسمى هذا النمط مصنع المزخرفات.

```
import functools

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

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping، ping، ping — ثلاثة أسطر
```

ترتيب الاستدعاءات: `repeat(times=3)` تعيد `decorator`، وهذا يُطبَّق على `ping` ويعيد `wrapper`. أي أن `@repeat(times=3)` تعني `ping = repeat(times=3)(ping)`.

مثال عملي: إعادة المحاولة عند الخطأ:

```
import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...
```

## عدة مزخرفات على دالة واحدة

يمكن وضع المزخرفات فوق بعضها، وتُطبَّق من الأسفل إلى الأعلى، فالأقرب إلى `def` يُطبَّق أولاً.

```
@timer
@log_calls
def report():
    ...

# الشيء نفسه:
report = timer(log_calls(report))
```

عند الاستدعاء تعمل الطبقة الخارجية (`timer`) أولاً، فتقيس زمن عمل `log_calls` أيضاً. والترتيب مهم: مثلاً إذا كان أحد المزخرفات يتحقق من الصلاحيات والآخر يخزن النتيجة مؤقتاً.

## المزخرفات المدمجة والقياسية في Python

توجد في اللغة والمكتبة القياسية مزخرفات كثيرة مفيدة:

| المزخرف | ماذا يفعل |
| --- | --- |

| `@property` | يحوّل الطريقة إلى خاصية للقراءة فقط؛ ومع `@x.setter` تصبح خاصية قابلة للضبط |

| `@staticmethod` | طريقة صنف بلا `self`، وهي دالة عادية في فضاء أسماء الصنف |

| `@classmethod` | تستقبل الطريقة الصنف `cls` بدلاً من الكائن، وبهذا تُكتب المُنشئات البديلة |

| `@functools.cache`، `@functools.lru_cache` | تخزّن النتائج حسب الوسائط |

| `@functools.wraps` | تنسخ البيانات الوصفية للدالة إلى الغلاف |

| `@dataclasses.dataclass` | تولّد `__init__` و`__repr__` و`__eq__` للصنف |

| `@contextlib.contextmanager` | تحوّل مولّداً إلى مدير سياق للعبارة `with` |

### التخزين المؤقت: functools.cache وlru_cache

يحفظ المزخرف التخزيني نتيجة كل مجموعة وسائط، وعند الاستدعاء المتكرر يعيدها دون حساب. والمثال الكلاسيكي أرقام فيبوناتشي التكرارية، التي تُحسب دون تخزين في وقت أُسّي:

```
import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075
```

تحفظ `functools.cache` (ظهرت في Python 3.9) كل النتائج دون حد. أما `@functools.lru_cache(maxsize=128)` فتحتفظ بآخر عدد من النتائج وتحذف القديمة، وهي أأمن للخدمات طويلة التشغيل. تُعرض الإحصاءات بـ `fib.cache_info()`، وتُفرَّغ بـ `fib.cache_clear()`. ويجب أن تكون وسائط هذه الدوال قابلة للتجزئة: قائمة وسيطاً تعطي `TypeError: unhashable type: 'list'`. ولا تخزّن إلا الدوال النقية، أي التي تعطي دائماً النتيجة نفسها لنفس الوسائط ولا تترك آثاراً جانبية.

### ماذا يفعل المزخرف property

يسمح `@property` بالوصول إلى الطريقة كأنها خاصية، دون أقواس، مع التحكم في الإسناد عند الحاجة:

```
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("الرصيد لا يمكن أن يكون سالباً")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError
```

### staticmethod وclassmethod

```
class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rubles(cls, rubles):
        return cls(round(rubles * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False
```

يتيح `@classmethod` للطريقة العمل مع الصنف نفسه لا مع كائناته: فالطريقة `from_rubles` تنشئ كائن الصنف الفرعي بشكل صحيح أيضاً.

## مزخرفات الأصناف وأصناف المزخرفات

يمكن تطبيق المزخرف على صنف: يستقبل الصنف ويعيد الصنف نفسه أو معدَّلاً. وهكذا يعمل `@dataclass`. ومثال بسيط هو سجل للأصناف:

```
registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': }
```

والوضع المعاكس أن يكون الصنف هو المزخرف. يصلح أي كائن فيه الطريقة `__call__`، ويمكن حفظ الحالة في خصائص الكائن:

```
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2
```

## مزخرف للدوال غير المتزامنة

بالنسبة لدوال `async def` يجب أن يكون الغلاف أيضاً دالة مشتركة (coroutine) وينتظر النتيجة بـ `await`، وإلا ستعود كائن الدالة غير المنفَّذة:

```
import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"بدأت {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"انتهت {func.__name__}")
        return result
    return wrapper
```

## لماذا نستخدم المزخرفات في Python

- **المنطق المشترك**: التسجيل والمقاييس وقياس الزمن دون نسخ الكود في كل دالة.
- **التخزين المؤقت**: `@functools.cache` للحسابات النقية المكلفة.
- **التحقق**: الصلاحيات والتحقق من الوسائط والتفويض في معالجات الويب.
- **التسجيل**: مسارات Flask وFastAPI (`@app.get("/")`) وتركيبات pytest.
- **الموثوقية**: إعادة المحاولة والمهلات الزمنية ومعالجة الأخطاء.

أهم فائدة هي فصل المسؤوليات: تقوم الدالة بعملها، ويُضاف السلوك الإضافي بسطر واحد، ويُزال بالسهولة نفسها.

## أخطاء شائعة

- **الغلاف لا يعيد النتيجة**: الدالة بعد التزخرف تعيد `None`.
- **غياب `*args, **kwargs`**: `TypeError` عند الاستدعاء بوسائط.
- **نسيان `wraps`**: اسم وتوثيق خاطئان، وتشويش في السجلات.
- **نسيان الأقواس في المصنع**: كتابة `@repeat` بدلاً من `@repeat(3)` تجعل الدالة تقع في المعامل `times`.
- **كود ثقيل داخل جسم المزخرف**: يُنفَّذ عند الاستيراد لا عند الاستدعاء.

## الخلاصة

المزخرفات في Python دوال عليا: تستقبل دالة وتعيد غلافاً، والرمز `@` مجرد اختصار لعملية إسناد. ابدأ بالقالب الذي فيه `*args, **kwargs` و`return result` و`functools.wraps`، وللإعدادات أضف مستوى المصنع. والدوال والإغلاقات والتلميحات التي تقوم عليها المزخرفات تُشرح عملياً في دورة بايثون.

## FAQ

**هل يمكن إزالة المزخرف من دالة؟**

إذا استخدم المزخرف functools.wraps فالأصل متاح عبر الخاصية __wrapped__: تستدعي func.__wrapped__(...) الدالة دون الغلاف. وإلا فلا يبقى إلا حذف سطر @ من الكود.

**متى يُنفَّذ كود المزخرف؟**

جسم المزخرف نفسه يُنفَّذ مرة واحدة عند تعريف الدالة، عادةً أثناء استيراد الوحدة. أما الكود داخل wrapper فيُنفَّذ مع كل استدعاء للدالة المزخرفة.

**هل يمكن تزخرف طرائق الصنف؟**

نعم، يعمل المزخرف العادي ذو *args, **kwargs مع الطرائق أيضاً، إذ يصبح self أول عنصر في args.

**ما الفرق بين المزخرف والوراثة؟**

الوراثة تغيّر سلوك الصنف عبر التسلسل الهرمي، أما المزخرف فيغلّف دالة أو صنفاً محدداً من الخارج، ويتركب بسهولة مع مزخرفات أخرى دون تعديل الكود.


---
Source: Skillaria.Top (https://skillaria.top)
