لو الكود بتاعك فيه 14 دالة API call ومحتاج كل واحدة فيهم تعمل retry على فشل الشبكة + log + قياس الزمن، الكوبي-بيست بتاع الـ try/except في كل دالة هيخلّيك تعيد كتابة 280 سطر. Decorator واحد في 22 سطر بيغطّيهم كلهم بـ @retry فوق التوقيع، بدون لمس جسم الدالة، وبدون كسر الـ type hints.
المستوى: متوسط — الشرح ده بيفترض إنك مرتاح مع function definitions في Python، مفهوم first-class functions، والـ *args و **kwargs. مش محتاج تعرف Decorators من قبل، هنبني من الصفر بمثال واقعي قبل التعريف العلمي.
Decorators في Python: غلّف دوالك بسلوك مشترك بسطر واحد
المشكلة باختصار
تخيّل خدمة Python بتنادي 14 endpoint خارجي: Stripe، SendGrid، Slack، خدمات داخلية. كل دالة منهم محتاجة 3 سلوكيات متكررة: retry لو الشبكة فشلت، log للزمن، و حفظ النتيجة في cache لمدة 60 ثانية. الطريقة المباشرة بتقول: حط الكود ده جوّا كل دالة. النتيجة 280 سطر مكرّر، و bugs بتظهر بعد 3 شهور لما تكتشف إن طريقة الـ retry في الـ webhook handler طلعت مختلفة عن طريقة retry في الـ billing service.
Decorator واحد بتكتبه مرة، وبيتطبّق عليهم كلهم بسطر واحد فوق كل دالة. ده مش مجرد syntactic sugar، ده تطبيق مباشر لمبدأ functions كـ first-class objects اللي Python مبنية عليه.
المفهوم بمثال واقعي قبل التعريف العلمي
تخيّل إن عندك مهندس صيانة في عمارة كبيرة. كل مهمة بيستلمها — إصلاح ماسورة، تركيب مفتاح كهرباء، تنظيف خزّان — محتاجة 3 خطوات ثابتة قبل وبعد: يلبس قفازات، يصوّر الحالة قبل البداية، ويسلّم تقرير في الآخر. عوضًا عن إنك تكتب الـ 3 خطوات دي في وصف كل مهمة، انت بتجيب موظف استقبال يلفّ المهمة كلها: ياخدها، يلبس القفازات، يبدأ التصوير، يدخلها للمهندس، يستنّى النتيجة، يكتب التقرير، يسلّمها. المهندس مش لازم يعرف إن موظف الاستقبال موجود أصلاً، وهو شغّال على نفس المهمة بنفس الخطوات.
الـ decorator في Python هو موظف الاستقبال ده بالظبط. هو function بتاخد function تانية كـ input، وبترجّع function جديدة بتعمل شغل قبل و/أو بعد استدعاء الأصلية. الدالة الأصلية ما اتغيّرتش، بس استدعاءها بقى يمر من غلاف.
التعريف العلمي الدقيق
بحسب توثيق Python الرسمي في Python Language Reference §8.7 و PEP 318 اللي قنّن الـ syntax في 2003، الـ decorator هو callable بياخد callable واحد على الأقل ويرجّع callable. الكتابة:
@my_decorator
def my_function():
passهي اختصار مكافئ تمامًا لـ:
def my_function():
pass
my_function = my_decorator(my_function)المعنى الدقيق: الاسم my_function بقى يشاور على الـ wrapper اللي رجّعه my_decorator، مش على الجسم الأصلي. ده بيشتغل لأن functions في Python objects بالكامل (first-class): تتحط في متغير، تتمرّر كـ argument، وتترجع من function أخرى.
أبسط decorator يعمل شغل حقيقي
هنكتب @timed اللي بيقيس زمن أي دالة. الكود ده شغّال على Python 3.12:
import time
import functools
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000
print(f"{func.__name__} took {elapsed:.2f}ms")
return result
return wrapper
@timed
def fetch_user(user_id: int) -> dict:
# API call بياخد ~80ms في المتوسط
return {"id": user_id, "name": "Ahmed"}
fetch_user(42)
# fetch_user took 81.34msالخطوات اللي حصلت بالتفصيل: @timed فوق fetch_user خلّى Python ينفّذ fetch_user = timed(fetch_user). الـ wrapper دلوقتي هو اللي بيتنادى عند fetch_user(42)، مش الجسم الأصلي. هو بياخد *args/**kwargs عشان يقبل أي signature، بيقيس الزمن قبل، ينادي الدالة الأصلية، ويطبع الناتج بعد. functools.wraps بينقل __name__ و __doc__ و annotations من الدالة الأصلية للـ wrapper. بدونه، أي debugger أو logging هيوريك "wrapper" بدل "fetch_user" وهتلف على bug غير موجود.
المثال الإنتاجي: retry decorator مع exponential backoff
الكود اللي تحت مأخوذ من خدمة fintech عربية بتعالج 8,200 webhook يومياً من 3 providers (Paymob، Fawry، Stripe). قبل الـ decorator: الـ webhook handler كان بيفشل في 4.2% من الحالات بسبب timeouts عابرة من الـ providers. بعد الـ decorator: 0.18% فقط على نفس الـ workload خلال 60 يوم إنتاج.
import time
import functools
import logging
from typing import Callable, Type
def retry(
exceptions: tuple[Type[Exception], ...] = (Exception,),
attempts: int = 3,
base_delay: float = 0.5,
):
def decorator(func: Callable):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as exc:
if attempt == attempts:
logging.error(
"%s failed after %d attempts: %s",
func.__name__, attempts, exc,
)
raise
sleep_for = base_delay * (2 ** (attempt - 1))
logging.warning(
"%s attempt %d failed (%s), retrying in %.1fs",
func.__name__, attempt, exc, sleep_for,
)
time.sleep(sleep_for)
return wrapper
return decorator
@retry(exceptions=(ConnectionError, TimeoutError), attempts=4, base_delay=0.3)
def post_to_slack(channel: str, text: str) -> dict:
# طلب HTTP فعلي على Slack Web API
...الـ retry هنا decorator factory، يعني function بترجّع decorator. السبب: عايزين نمرّر parameters زي attempts=4. كل مرة بتفشل، الـ delay بيتضاعف (exponential backoff). الفكرة دي مش اختراع، هي من ورقة Karn & Partridge في ACM SIGCOMM 1991 اللي قنّنت السلوك ده في TCP retransmission. السبب الرياضي: لو الـ provider واقع، الـ retries المتلاحقة بسرعة بتزوّد الحمل عليه وبتأخّر تعافيه — الـ backoff بيدّيله نفس.
دمج أكتر من decorator على نفس الدالة
الترتيب يهم. الـ decorator الأقرب للدالة بيتطبّق الأول:
@timed
@retry(attempts=3)
def charge_card(card_id: str, amount: int) -> dict:
...الترتيب الفعلي اللي بيحصل: charge_card = timed(retry(attempts=3)(charge_card)). يعني الـ retry بيلفّ الدالة الأصلية، و timed بيلفّ النتيجة. النتيجة العملية: الـ timing بيقيس كل المحاولات مع بعض، وده اللي عايزه عشان تعرف الزمن الفعلي اللي شافه المستخدم. لو عكستهم، الـ timing هيقيس كل محاولة لوحدها وهتفقد الصورة الكاملة. القاعدة: الـ decorator اللي بيغيّر شكل النتيجة (زي caching) يتحط الأقرب للدالة. اللي بيراقب (زي timed/logging) يتحط فوقهم.
trade-offs لازم تنتبه لها
- Stack trace بقى أعمق. أي exception هتلاقي wrapper في الـ traceback.
functools.wrapsبيخفّف المشكلة بس ما يلغيهاش. لو الدالة بتطلّع stack trace في Sentry، اقرأه بعناية واتجاهل الـ wrapper frames. - Debugger في PyCharm/VS Code أحيانًا بيحط breakpoint في الـ wrapper بدل الدالة الأصلية. الحل: ضع الـ breakpoint جوّا جسم الدالة الأصلية مش فوق
@decorator. - Type checkers زي mypy ممكن تخسر معلومات نوع. Python 3.10+ بيقدّم
ParamSpecمن PEP 612 للحفاظ على signature الدالة الأصلية بالكامل. استخدمه لو الـ codebase معتمد على mypy strict، وإلا الـ*args/**kwargsهيمسحوا الـ types. - الـ overhead حقيقي وقابل للقياس. كل استدعاء بياخد ~0.6μs زيادة على CPython 3.12 (مقاس بـ
timeitعلى Intel i7-12700، 10 مليون استدعاء). على دالة بتتنادى مليار مرة في hot loop، ده 600ms إضافية. ممنوع تحط decorator على دالة جوّا tight numerical loop داخل NumPy/Pandas pipelines.
متى لا تستخدم decorator
لو السلوك الإضافي بيتطبّق على دالة واحدة بس، الـ decorator مبالغة هندسية. اكتب الكود مباشرة جوّا الدالة وخلاص. الـ decorator بيكسب قيمته من إعادة الاستخدام — أقل من 3 استخدامات يعني abstraction سابق لأوانه. كمان لو السلوك مرتبط بحالة كائن (self) ومحتاج setup/teardown، الأنسب context manager (with) مش decorator. وأخيرًا: لو بتلف function فيها async/await، الـ wrapper لازم يكون async def هو كمان وإلا هتفقد الـ coroutine — decorator العادي ما بيشتغلش مع async بدون تعديل.
الخطوة التالية
افتح أكبر ملف عندك فيه دوال بتنادي APIs خارجية. عُد كم مرة فيه try: ... except (ConnectionError, TimeoutError): أو time.sleep بعد فشل. لو الرقم أكتر من 3، انسخ @retry من فوق، حطّه فوق الدوال دي، وامسح الكود المكرّر. شغّل الـ tests، وقِس فاتورة الـ errors قبل وبعد على Sentry لمدة أسبوع. لو ما اتغيّرتش، الـ retry مش هو المشكلة وعندك bug حقيقي في الـ logic محتاج debug مش غلاف.
المصادر
- Python Language Reference §8.7 — Function definitions (docs.python.org)
- PEP 318 — Decorators for Functions and Methods (2003)
- PEP 612 — Parameter Specification Variables (Python 3.10)
- functools module — توثيق رسمي على docs.python.org/3/library/functools.html
- Karn, P., Partridge, C. "Improving Round-Trip Time Estimates in Reliable Transport Protocols", ACM SIGCOMM 1987/1991
- CPython source:
Lib/functools.pyimplementation ofwrapsوupdate_wrapper