الديكوريترز في بايثون ليست مجرد سكر نحوي، بل أداة قوية تُغير طريقة تفكيرك في الكود. سنفكك معاً كيف تعمل خلف الكواليس، وننتقل من الأمثلة البسيطة إلى الاستخدامات المتقدمة التي تُستخدم في مكتبات شهيرة مثل FastAPI وDjango.
تخيل أنك تعمل على مشروع كبير يستخدم FastAPI، وفجأة تجد نفسك تكتب نفس الكود للتحقق من صلاحيات المستخدم في كل دالة. ليس فقط أن الكود يتكرر، بل أن أي تعديل يتطلب تغيير عشرات الأماكن. هنا يأتي دور الديكوريترز - إنها ليست مجرد طريقة لتجميل الكود، بل أداة حقيقية لتجنب تكرار المنطق وتحويل الكود من فوضى متشابكة إلى هيكلية نظيفة. المشكلة الحقيقية ليست في كتابة الديكوريتر نفسه، بل في فهم كيف يعمل خلف الكواليس وكيف يمكن استخدامه بطريقة احترافية دون الوقوع في الفخاخ الشائعة.
في هذا المقال، لن نتوقف عند شرح الديكوريترز كسطر '@' فوق الدالة. سنذهب أعمق: كيف تُخزن الدوال في الذاكرة؟ كيف يتعامل بايثون مع الـ First-class functions؟ وما الفرق بين الديكوريتر العادي والديكوريتر الذي يقبل باراميترات؟ سنرى أمثلة عملية من مكتبات حقيقية وكيف يمكن استخدام الديكوريترز لتحسين الأداء، والتعامل مع الـ I/O Bound operations، وحتى لتصحيح الأخطاء في الإنتاج.
الكثير من المطورين يرون الديكوريترز كشيء غامض، لكن الحقيقة أبسط بكثير. في بايثون، الدوال هي كائنات من الدرجة الأولى (First-class objects)، وهذا يعني أنه يمكنك تمريرها كأي متغير آخر، تخزينها في قوائم، وإرجاعها من دوال أخرى. الديكوريتر ليس أكثر من دالة تأخذ دالة أخرى وتعيد دالة معدلة. عندما تكتب '@retry' فوق دالة، فأنت في الواقع تقول لبايثون: 'استبدل هذه الدالة بالنتيجة التي ترجعها دالة retry عند تمرير الدالة الأصلية لها'.
لنأخذ مثالاً بسيطاً لكن عملياً: ديكوريتر لقياس زمن تنفيذ الدالة. قد يبدو المثال تافهاً، لكن فكر في كم مرة تحتاج لقياس الأداء في الإنتاج. بدلاً من كتابة 'start = time.time()' في كل دالة، يمكنك استخدام ديكوريتر واحد. لكن هنا يأتي السؤال: ماذا لو كانت الدالة ترجع قيمة؟ كيف نحافظ على القيمة المرجعة دون فقدانها؟ هذا هو التحدي الحقيقي في كتابة ديكوريترز جيدة - ليس فقط كتابة المنطق، بل الحفاظ على سلوك الدالة الأصلية.
import time
from functools import wraps
def timer(func):
@wraps(func) # يحافظ على اسم الدالة ومعلوماتها الأصلية
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # تنفيذ الدالة الأصلية
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result # إرجاع النتيجة الأصلية
return wrapper
@timer
def calculate_factorial(n):
return 1 if n <= 1 else n * calculate_factorial(n - 1)
print(calculate_factorial(10)) # سيطبع زمن التنفيذ والنتيجةلاحظ استخدام '@wraps' من مكتبة functools. الكثير من المطورين يهملونها، لكن بدونها ستفقد الدالة الأصلية اسمها ومعلوماتها، وهذا يسبب مشاكل في الـ Debugging وفي مكتبات مثل FastAPI التي تعتمد على أسماء الدوال. هذه التفاصيل الصغيرة هي ما يميز الديكوريترز الجيدة عن السيئة. أيضاً، لاحظ كيف استخدمنا 'time.perf_counter()' بدلاً من 'time.time()' - هذا لأن perf_counter أكثر دقة لقياس زمن التنفيذ القصير، وهو ما يهمنا في تحسين الأداء.
الديكوريترز البسيطة سهلة، لكن ما يحدث عندما تحتاج لتمرير باراميترات للديكوريتر نفسه؟ مثلاً، ديكوريتر لإعادة المحاولة (retry) الذي يقبل عدد المحاولات وزمن الانتظار بين المحاولات. هنا تصبح الأمور معقدة قليلاً، لأن الديكوريتر العادي لا يقبل باراميترات. الحل هو إضافة مستوى آخر من الدوال - دالة خارجية ترجع الديكوريتر الفعلي. هذا النمط يسمى 'ديكوريتر فاكتوري' (Decorator Factory).
لنأخذ مثالاً عملياً من مكتبة requests. تخيل أنك تريد ديكوريتر لإعادة المحاولة عند فشل الاتصال بالسيرفر، مع تحديد عدد المحاولات وزمن الانتظار. بدون ديكوريتر، ستكتب نفس الكود في كل مكان. مع ديكوريتر، يمكنك كتابة المنطق مرة واحدة واستخدامه في كل مكان. لكن هنا تأتي المشكلة: كيف تتعامل مع الاستثناءات؟ هل تعيد المحاولة على كل استثناء؟ أم فقط على استثناءات معينة مثل ConnectionError؟ وكيف تتعامل مع الـ Exponential Backoff؟ هذه هي الأسئلة الحقيقية التي تواجهك في الإنتاج.
import time
from functools import wraps
def retry(max_attempts=3, delay=1, excepti(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
print(f"Attempt {attempt} failed: {e}. Retrying in {delay} seconds...")
time.sleep(delay)
print(f"All {max_attempts} attempts failed")
raise last_exception
return wrapper
return decorator
@retry(max_attempts=5, delay=2, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
import requests
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
# fetch_data("https://api.example.com/data") # سيحاول 5 مرات مع انتظار 2 ثوان بين المحاولاتلاحظ كيف أن الديكوريتر يقبل الآن باراميترات، وهذا يتطلب مستوى إضافياً من الدوال. أيضاً، لاحظ كيف نتعامل مع الاستثناءات - بدلاً من إعادة المحاولة على كل استثناء، نحدد الاستثناءات التي نريد إعادة المحاولة عليها. هذا مهم جداً في الإنتاج، لأنك لا تريد إعادة المحاولة على استثناءات مثل ValueError مثلاً. أيضاً، لاحظ كيف نستخدم 'raise last_exception' بدلاً من مجرد طباعة الخطأ - هذا لأننا نريد أن يعرف الكود الذي يستدعي الدالة أن هناك خطأ، بدلاً من إخفاء الخطأ.
الديكوريترز ليست مجرد أداة لتجميل الكود، بل هي جزء أساسي من العديد من المكتبات الشهيرة. لنأخذ FastAPI كمثال - كل دالة في FastAPI تستخدم ديكوريترز مثل '@app.get' و'@app.post'. لكن كيف يعمل هذا خلف الكواليس؟ عندما تكتب '@app.get("/items/{item_id}")'، فأنت في الواقع تمرر الدالة إلى ديكوريتر يقوم بتسجيلها في قائمة الـ Routes الخاصة بالتطبيق. هذا ليس مجرد سكر نحوي، بل طريقة لتنظيم الكود وجعل الـ Routing أكثر وضوحاً.
مثال آخر من Django - الديكوريتر '@login_required'. بدلاً من كتابة الكود للتحقق من تسجيل الدخول في كل دالة، يمكنك استخدام الديكوريتر. لكن هنا تأتي المشكلة: ماذا لو كنت تريد السماح ببعض المستخدمين غير المسجلين بالدخول إلى صفحة معينة؟ كيف تعدل سلوك الديكوريتر؟ هذا هو التحدي الحقيقي - ليس فقط استخدام الديكوريترز، بل تعديلها لتتناسب مع احتياجات مشروعك. في Django، يمكنك كتابة ديكوريتر مخصص يتحقق من مجموعة معينة من المستخدمين بدلاً من مجرد تسجيل الدخول.
from functools import wraps
from django.http import HttpResponseForbidden
def group_required(*group_names):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden("Login required")
if not request.user.groups.filter(name__in=group_names).exists():
return HttpResponseForbidden("Permission denied")
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
@group_required('admins', 'editors')
def admin_dashboard(request):
return HttpResponse("Welcome to the admin dashboard")لاحظ كيف أن هذا الديكوريتر يقبل باراميترات (أسماء المجموعات)، وكيف يتحقق من تسجيل الدخول ومن عضوية المجموعة. هذا مثال واقعي على كيف يمكن استخدام الديكوريترز للتحكم في الوصول بطريقة نظيفة ومرنة. أيضاً، لاحظ كيف نستخدم 'HttpResponseForbidden' بدلاً من إعادة توجيه المستخدم إلى صفحة تسجيل الدخول - هذا لأننا نريد أن يعرف المستخدم سبب رفض الوصول، بدلاً من مجرد إعادة توجيهه دون تفسير.
واحدة من أقوى استخدامات الديكوريترز هي التعامل مع الـ I/O Bound operations، مثل طلبات الشبكة أو قراءة الملفات. بدلاً من كتابة نفس الكود في كل مكان، يمكنك استخدام ديكوريتر لتشغيل الدالة في ثريد منفصل أو باستخدام async/await. لكن هنا تأتي المشكلة: كيف تتعامل مع الـ Thread Safety؟ وكيف تضمن أن الدالة لا تُنفذ أكثر من مرة في نفس الوقت؟
لنأخذ مثالاً عملياً: ديكوريتر لتشغيل دالة في ثريد منفصل مع تحديد زمن الانتظار الأقصى. هذا مفيد جداً في التطبيقات التي تتعامل مع طلبات الشبكة أو العمليات الطويلة. لكن لاحظ أن هذا ليس حلاً سحرياً - يجب أن تفهم كيف تعمل الثريدات في بايثون وكيف تتعامل مع الـ GIL. أيضاً، يجب أن تفهم الفرق بين الـ I/O Bound والـ CPU Bound operations، لأن الديكوريترز ليست الحل الأمثل لكل شيء.
import threading
from functools import wraps
def run_in_thread(timeout=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result_c []
exception_container = []
def target():
try:
result_container.append(func(*args, **kwargs))
except Exception as e:
exception_container.append(e)
thread = threading.Thread(target=target)
thread.start()
thread.join(timeout=timeout)
if thread.is_alive():
raise TimeoutError(f"Function {func.__name__} timed out after {timeout} seconds")
if exception_container:
raise exception_container[0]
return result_container[0] if result_container else None
return wrapper
return decorator
@run_in_thread(timeout=5)
def fetch_large_file(url):
import requests
response = requests.get(url, stream=True)
response.raise_for_status()
return response.content
# content = fetch_large_file("https://example.com/large_file.zip")لاحظ كيف نستخدم قوائم ('result_container' و'exception_container') لتخزين النتيجة والاستثناءات، لأن الثريدات لا تستطيع إرجاع قيم مباشرة. أيضاً، لاحظ كيف نتعامل مع الـ Timeout - بدلاً من ترك الثريد يعمل إلى الأبد، نحدد زمناً أقصى ونرفع استثناءً إذا تجاوز الزمن المحدد. هذه التفاصيل الصغيرة هي ما يجعل الديكوريترز مفيدة في الإنتاج، بدلاً من مجرد أداة للتجارب البسيطة.
الديكوريترز قوية، لكنها مليئة بالفخاخ التي يمكن أن تسبب مشاكل غريبة في الإنتاج. واحدة من أكبر المشاكل هي فقدان معلومات الدالة الأصلية - بدون '@wraps'، ستفقد الدالة اسمها ومعلوماتها، وهذا يسبب مشاكل في الـ Debugging وفي مكتبات مثل FastAPI وDjango. أيضاً، الديكوريترز يمكن أن تسبب مشاكل في الأداء إذا لم تُكتب بعناية - مثلاً، إذا كان الديكوريتر ينفذ كوداً ثقيلاً في كل استدعاء، فهذا سيؤثر على أداء التطبيق.
مشكلة أخرى شائعة هي الديكوريترز التي تتداخل مع بعضها البعض. مثلاً، إذا استخدمت ديكوريتر لتسجيل الدخول وآخر لقياس الزمن، يجب أن تتأكد أن الديكوريترز لا تتداخل بطريقة تسبب مشاكل. القاعدة العامة هي: الديكوريتر الذي يعدل سلوك الدالة (مثل تسجيل الدخول) يجب أن يوضع فوق الديكوريتر الذي يقيس الزمن أو يسجل الأخطاء. أيضاً، يجب أن تكون حذراً مع الديكوريترز التي تقبل باراميترات - إذا لم تُكتب بعناية، يمكن أن تسبب مشاكل في الـ Closure Scope.
واحدة من أخطر المشاكل التي يمكن أن تسببها الديكوريترز هي الـ Memory Leaks، خاصة عندما تتعامل مع كائنات كبيرة أو موارد خارجية مثل الملفات أو اتصالات الشبكة. المشكلة تحدث عندما تحتفظ الديكوريتر بمرجع إلى الدالة الأصلية أو إلى أحد باراميتراتها، مما يمنع الـ Garbage Collector من تحرير الذاكرة. مثلاً، إذا كان الديكوريتر يخزن نتيجة الدالة في قائمة خارجية، فهذا يمكن أن يسبب تسرباً في الذاكرة إذا لم تُحرر القائمة بشكل صحيح.
لنأخذ مثالاً: ديكوريتر لتخزين نتائج الدوال في ذاكرة مؤقتة (Caching). هذا مفيد جداً لتحسين الأداء، لكن إذا لم تُكتب بعناية، يمكن أن يسبب تسرباً في الذاكرة. الحل هو استخدام مكتبات مخصصة للـ Caching مثل functools.lru_cache، أو كتابة الديكوريتر بعناية بحيث لا يحتفظ بمراجع غير ضرورية. أيضاً، يجب أن تكون حذراً مع الديكوريترز التي تستخدم الثريدات أو الـ Async/Await، لأنها يمكن أن تسبب تسربات في الذاكرة إذا لم تُغلق بشكل صحيح.
from functools import wraps
def cache_results(max_size=128):
cache = {}
def decorator(func):
@wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
if len(cache) >= max_size:
cache.clear() # تجنب تسرب الذاكرة
cache[args] = result
return result
return wrapper
return decorator
@cache_results(max_size=100)
def expensive_calculation(x):
return x * x # مثال بسيط - في الواقع قد تكون عملية معقدةلاحظ كيف نستخدم 'cache.clear()' عندما تصل القائمة إلى الحجم الأقصى - هذا لتجنب تسرب الذاكرة. أيضاً، لاحظ أن هذا المثال بسيط ولا يتعامل مع الـ Thread Safety - في الإنتاج، يجب أن تستخدم مكتبة متخصصة للـ Caching مثل functools.lru_cache أو Redis. الديكوريترز الجيدة يجب أن تكون بسيطة ومباشرة، وإذا أصبحت معقدة، فمن الأفضل استخدام مكتبة متخصصة بدلاً من كتابة الكود بنفسك.
بعد أكثر من عشر سنوات في كتابة بايثون، هذه هي نصائحي الذهبية لاستخدام الديكوريترز باحترافية: ابدأ دائماً بكتابة الديكوريتر بدون '@' لتفهم كيف يعمل، ثم أضف السكر النحوي لاحقاً. استخدم '@wraps' دائماً - لا استثناءات. إذا كان الديكوريتر يقبل باراميترات، اكتب اختباراً لكل مجموعة من الباراميترات للتأكد من أنه يعمل بشكل صحيح. واختبر الديكوريترز في بيئة مشابهة للإنتاج - خاصة تلك التي تتعامل مع الثريدات أو الـ Async/Await.
الديكوريترز ليست مجرد أداة لتجميل الكود، بل طريقة لتحويل الكود المتكرر والمعقد إلى هيكلية نظيفة ومرنة. لكن تذكر: الديكوريتر الجيد يجب أن يكون بسيطاً، واضحاً، وسهل الفهم. إذا وجدت نفسك تكتب ديكوريتر معقد جداً، فمن الأفضل تقسيمه إلى عدة دوال أو استخدام مكتبة متخصصة. وفي النهاية، الديكوريترز هي مجرد أداة - الأداة الجيدة هي التي تجعل الكود أفضل، وليس أسوأ.
البرمجة ليست عن كتابة الكود، بل عن حل المشاكل. الديكوريترز هي أداة قوية لحل مشاكل التكرار والتعقيد، لكن استخدامها بشكل صحيح يتطلب فهماً عميقاً لكيفية عمل بايثون خلف الكواليس.
— مطور بايثون محترف