المستوى المطلوب: متوسط — هذا الشرح موجّه لمطوّر Python يعرف الدوال والـ scope الأساسي، وعنده فضول يفهم ليه السطر @app.get("/users") في FastAPI شغّال أصلاً.
لو عندك 14 دالة في API، وكل واحدة محتاجة تكتب logging و retry على فشل الشبكة و cache للنتيجة، انت قدامك خياران: تنسخ نفس 12 سطر فوق كل دالة، أو تكتب 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...)، ويرجّع لك دالة جديدة لها نفس الواجهة. اللي بيستدعي الدالة مش حاسس إن فيه طبقة جوّاها.
التعريف العلمي الدقيق
الـ 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: قياس زمن التنفيذ
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