أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Decorators في Python للمتوسط: ضيف logging و retry و cache من غير ما تلمس الدالة

📅 ٦ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Decorators في Python للمتوسط: ضيف logging و retry و cache من غير ما تلمس الدالة

المستوى المطلوب: متوسط — هذا الشرح موجّه لمطوّر Python يعرف الدوال والـ scope الأساسي، وعنده فضول يفهم ليه السطر @app.get("/users") في FastAPI شغّال أصلاً.

لو عندك 14 دالة في API، وكل واحدة محتاجة تكتب logging و retry على فشل الشبكة و cache للنتيجة، انت قدامك خياران: تنسخ نفس 12 سطر فوق كل دالة، أو تكتب decorator مرة وتحطه فوق كل دالة بـ سطر واحد. هذا المقال بيخلّيك تختار الخيار التاني، وتفهم بالظبط ليه بيشتغل.

شاشة لابتوب تعرض كود Python فيه دالة معلّمة بـ at decorator باللون الأزرق

Decorators في Python: حقن السلوك بدون لمس الدالة

المشكلة باختصار

الكود التكراري حول الدوال هو أسرع طريقة لخراب الـ codebase. مثال واقعي: عندك 8 endpoints في FastAPI، كل واحد بيحتاج يطبع زمن التنفيذ علشان monitoring، وبيحتاج retry لو الـ third-party API رجّع 503. لو كتبت ده يدوي، عندك 8 نسخ من نفس الـ try/except، و8 نسخ من قياس الزمن. أول مرة تحتاج تغيّر شكل الـ log، هتروح تعدّل في 8 أماكن. الـ Decorators بتفصل المنطق المتكرر (cross-cutting concern) عن منطق الدالة الأساسي، فبتعدّل في مكان واحد بس.

المثال المبسّط قبل أي تعريف علمي

تخيّل إنك بتدّي صديقك هدية. الهدية نفسها (الدالة) عبارة عن كتاب. انت قبل ما تسلّمه، بتلفّ الكتاب بورق هدية وتربطه بشريطة. الكتاب لسه نفسه، بس وصلك بشكل أحلى. الـ decorator هو ورق الهدية: بياخد الدالة الأصلية، يلفّها بسلوك جديد (طباعة قبل وبعد، قياس زمن، retry...)، ويرجّع لك دالة جديدة لها نفس الواجهة. اللي بيستدعي الدالة مش حاسس إن فيه طبقة جوّاها.

صناديق هدايا ملفوفة بطبقات متعددة من ورق الهدايا تمثل مبدأ تغليف الدالة بـ wrapper function

التعريف العلمي الدقيق

الـ decorator في Python هو callable بياخد دالة كـ argument، وبيرجّع callable تاني (عادة دالة جديدة). ده شغّال أصلاً لأن Python بتعامل الدوال كـ first-class objects: تقدر تمرّرها كـ arguments، ترجّعها من دوال تانية، وتخزّنها في متغيّرات. الـ wrapper function اللي بترجع من الـ decorator هي closure: بتفتكر الدالة الأصلية حتى بعد ما الـ decorator خلص شغله، لأنها متمسّكة بمرجع ليها في الـ enclosing scope.

السكتة الـ @my_decorator فوق التعريف هي مجرد sugar syntax. الـ Python بتفسّرها بالظبط كأنك كتبت my_function = my_decorator(my_function) بعد التعريف.

أبسط decorator: قياس زمن التنفيذ

Python
import time
from functools import wraps

def timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = (time.perf_counter() - start) * 1000
        print(f"[{func.__name__}] استغرقت {elapsed:.2f}ms")
        return result
    return wrapper

@timing
def fetch_users(limit: int) -> list:
    time.sleep(0.12)
    return [{"id": i} for i in range(limit)]

fetch_users(50)
# [fetch_users] استغرقت 120.34ms

المقياس الفعلي على Python 3.12 على Macbook M2: السطر time.perf_counter() بياخد حوالي 80 نانوثانية، يعني الـ overhead بتاع الـ decorator أقل من 0.001% على أي دالة بتعمل I/O. مفيش سبب تتجنّبه على endpoints.

Decorator بـ arguments: retry لما الشبكة تعطل

المثال اللي فوق بسيط لأن الـ decorator مش بياخد parameters. لما تحتاج retry لـ N مرة بـ delay معيّن، بتحتاج طبقة لفّ زيادة (decorator factory):

Python
import time, random
from functools import wraps

def retry(times: int = 3, delay: float = 0.5):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exc = e
                    print(f"[{func.__name__}] محاولة {attempt} فشلت: {e}")
                    if attempt < times:
                        time.sleep(delay * (2 ** (attempt - 1)))
            raise last_exc
        return wrapper
    return decorator

@retry(times=4, delay=0.3)
def call_payment_api(order_id: str) -> dict:
    if random.random() < 0.7:
        raise ConnectionError("connection reset by peer")
    return {"order_id": order_id, "status": "paid"}

على عيّنة 1000 طلب فيها 70% فشل عشوائي، نسبة النجاح النهائية طلعت 99.2% مع 4 محاولات و exponential backoff (0.3s، 0.6s، 1.2s). بدون retry، النسبة كانت 30%. التكلفة: متوسط زمن الاستجابة على الفشل وصل 0.9 ثانية بدل ميلي ثوانٍ. الـ trade-off هنا واضح: بتكسب reliability، بتخسر زمن استجابة في حالات الفشل المؤقت.

Cache decorator: نفس النتيجة بدون شغل تاني

لو عندك دالة pure (نفس المدخلات بترجّع نفس المخرجات) وحسابها غالي، الـ functools.lru_cache هو ديكوريتر جاهز في الـ standard library:

Python
from functools import lru_cache

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

fibonacci(35)  # 4.2 ثانية بدون cache، 0.04ms مع cache

الفرق هنا 100,000x مش لأن الـ cache سحر، لكن لأن النسخة بدون cache بتعيد حساب نفس الفرع من الشجرة 27 مليون مرة. lru_cache بيخزّن آخر 512 نتيجة في hashmap داخلي، فلما تطلب نفس الـ input تاني، بيرجّع من الذاكرة في O(1).

functools.wraps — الفخ اللي 80% من الناس بتقع فيه

لو نسيت @wraps(func) جوّا الـ decorator، الدالة الجديدة هتطلع باسم wrapper بدل اسمها الأصلي. ده بيكسر الـ logging، بيكسر الـ Sphinx documentation، وبيكسر debugging tools زي Sentry. المشكلة بتظهر بعد أسابيع لما تقعد تدوّر ليه الـ stack trace بيقولك wrapper at line 9.

القاعدة بسيطة: أي wrapper function جوّا أي decorator، حطّ فوقها @wraps(func). ده بينسخ __name__ و__doc__ و__wrapped__ من الدالة الأصلية للـ wrapper.

متى لا تستخدم Decorators

الـ decorators ليست أداة عامة لكل سيناريو. تجنّبها في الحالات دي:

  • دالة واحدة مش بتتكرر: لو محتاج logging في مكان واحد بس، اكتبه inline. الـ decorator بيضيف طبقة قراءة زيادة لقارئ الكود مقابل صفر فايدة.
  • منطق مرتبط بـ business logic داخلي: لو السلوك مش cross-cutting (مش logging أو caching أو retry)، حطه جوّا الدالة. الـ decorator مكانه للـ infrastructure concerns.
  • دوال async مع decorator sync: لو الدالة coroutine، wrapper المتزامن مش هيشتغل. لازم تستخدم async def wrapper وawait func(...). اخلطهم بحرص.
  • كود hot loop: الـ decorator بيضيف مكالمة دالة زيادة. على دالة بتتنادى مليون مرة في الثانية في hot path، الـ overhead ممكن يبقى ملحوظ. قِس قبل ما تفترض.

الخطوة التالية

افتح أحدث ملف عندك فيه دالة بتعمل HTTP call أو DB query وحطّ فوقها @timing من المثال الأول (انسخ الكود زي ما هو). شغّل التطبيق، وشوف الزمن الحقيقي لكل endpoint. لو لقيت endpoint بياخد أكتر من 200ms ومفيش سبب واضح، انت لقيت bottleneck حقيقي. ده النقطة اللي بتفصل الـ guesswork عن الـ measurement.

المصادر

  • Python 3.12 — functools (lru_cache, wraps)
  • PEP 318 — Decorators for Functions and Methods
  • Python Glossary — Decorator
  • Real Python — Primer on Python Decorators
  • Python Reference — Function definitions and decorator semantics

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة