مستوى المقال: متوسط
لو فريقك بيكرّر نفس 8 سطور التحقق من JWT في 14 endpoint مختلف، انت بتدفع ضريبة DRY مرتين: مرة وقت كتابة الكود، ومرة تانية لما تيجي تعدّل سطر واحد فتلاقي نفسك بتغيّره في 14 مكان. الحل في Python موجود من سنة 2003 وبيتختصر في رمز واحد: @.
Decorators في Python: السطور المكررة تتحوّل لـ @ واحد
الـ Decorator هو دالة بتاخد دالة وترجّع دالة جديدة بسلوك إضافي. النتيجة العملية: تكتب الـ logic المشترك مرة واحدة، وتحطه على أي دالة بسطر @ في الأول. لا تكرار، ولا نسيان، ولا 14 ملف بتفتحهم لما تيجي تعدّل سطر واحد.
المشكلة باختصار
في خدمة fintech عربية شغّالة على FastAPI، الكود قبل Decorators كان فيه 14 endpoint، كل واحد فيهم بيبدأ بنفس الـ 8 سطور: قراءة Authorization header، فحص JWT، استخراج user_id، التحقق من الـ permissions. مجموع التكرار: 112 سطر هباء. لما الفريق قرّر يضيف rate limiting، اضطر يفتح 14 ملف ويضيف نفس السطور في كل واحد. واحد منهم اتنسي، والـ endpoint ده اتفتح للعالم 6 أيام قبل ما حد يلاحظ.
يعني إيه Decorator بمثال بسيط
تخيّل مبنى مكاتب فيه 14 شقة، وكل شقة عاوزة تتحقق من الزوار قبل ما يدخلوا. الحل الغبي: توظّف 14 موظف استقبال، كل واحد بيعمل نفس الفحوصات. الحل الذكي: تحط حارس واحد على البوابة الرئيسية، وكل زائر بيدخل من عنده الأول. الحارس مش بيغيّر طبيعة الشقق، هو بس بيضيف طبقة فحص قبل الدخول. لو محتاج تضيف فحص جديد (مثلًا قياس درجة الحرارة)، تعدّل حارس واحد بدل 14.
الـ Decorator في Python بيلعب دور الحارس. الدالة الأصلية بتفضل زي ما هي، والـ Decorator بيلف حواليها سلوك جديد قبل وبعد التنفيذ.
التعريف العلمي بدقة
وفقًا لـ PEP 318، الـ Decorator هو callable بياخد دالة كـ input ويرجّع callable تاني. الميكانيكية بتعتمد على مبدأين أساسيين في Python:
- الدوال first-class objects — بتتمرر كقيم زي أي متغير، وتتخزن في listsوdicts، وترجع من دوال تانية.
- الـ Closures — الدالة الداخلية بتقدر توصل لمتغيرات الدالة اللي حواليها حتى بعد ما الخارجية تخلص تنفيذها.
الـ @ مجرد syntactic sugar. لما تكتب @decorator فوق دالة، Python بيترجمها لـ func = decorator(func). كل اللي بيحصل تحت السطح هو إعادة إسناد للاسم.
أربع Decorators شغّالة في الإنتاج
1. @timer لقياس زمن التنفيذ
import time
from functools import wraps
def timer(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__} took {elapsed:.2f}ms")
return result
return wrapper
@timer
def fetch_users():
time.sleep(0.18)
return ["ahmed", "sara", "omar"]
fetch_users()
# fetch_users took 184.21ms