اكتشف كيف تحول الديكوريترز من مفهوم غامض إلى أداة لا غنى عنها في الكود الإنتاجي. أمثلة متقدمة تكشف أسرار الأداء، الـ Debugging، ودمجها مع مكتبات شهيرة مثل FastAPI وDjango.
في يوم ما، ستجد نفسك أمام كود بايثون يبدو وكأنه كتبه ساحر: دالة تُستدعى قبل أخرى، متغيرات تظهر من العدم، وسلوك يتغير دون تعديل السطور الأصلية. هذا ليس سحراً، بل هو الديكوريترز - أداة قوية لدرجة أن معظم المطورين يستخدمونها دون فهم حقيقي لكيفية عملها تحت الغطاء. المشكلة؟ عندما يفشل الكود في الإنتاج، ستجد نفسك تبحث عن الـ Memory Leak أو الـ Blocking Call في مكان خاطئ تماماً. الحقيقة هي أن الديكوريترز ليست مجرد سكر زخرفي، بل هي نمط برمجي متكامل يتحكم في تدفق التنفيذ، إدارة الموارد، وحتى سلوك الـ Event Loop نفسه.
لنبدأ بمثال كارثي: تخيل أن لديك واجهة برمجة تطبيقات FastAPI تعالج 10,000 طلب في الدقيقة، وكل طلب يستدعي دالة تسجيل الدخول التي تستخدم ديكوريتر لتسجيل الوقت. فجأة، تلاحظ أن الـ Response Time زاد من 50ms إلى 300ms. بعد ساعات من الـ Debugging، تكتشف أن الديكوريتر كان ينشئ ملف log جديد لكل طلب بدلاً من استخدام نفس الملف المفتوح. هذه ليست مشكلة في الديكوريتر نفسه، بل في كيفية استخدامه - تماماً مثل إعطاء سلاح نووي لطفل دون تعليمه قواعد السلامة.
عندما ترى @decorator فوق دالة، فإن ما يحدث خلف الكواليس يشبه عملية جراحية دقيقة. بايثون لا تعدل الدالة الأصلية، بل تنشئ كائن دالة جديد تماماً في الذاكرة، ثم تعيد ربط اسم الدالة الأصلية بهذا الكائن الجديد. هذا يعني أن الدالة الأصلية تختفي فعلياً من الـ Symbol Table، ويحل محلها النسخة المزينة. إذا حاولت الوصول إلى الدالة الأصلية بعد التزيين، ستجد نفسك أمام مفاجأة غير سارة - إلا إذا كنت قد احتفظت بالمرجع الأصلي مسبقاً.
لنأخذ مثالاً عملياً: عندما تكتب @timing فوق دالة process_data، فإن بايثون تنفذ الكود التالي خلف الكواليس: process_data = timing(process_data). هذا يعني أن الدالة process_data الأصلية تُمرر كوسيطة إلى الديكوريتر timing، الذي بدوره يعيد دالة جديدة مغلفة. هذه الدالة الجديدة هي التي ستُستدعى عند استخدام process_data لاحقاً. المشكلة هنا أن معظم المطورين ينسون أن الدالة الأصلية اختفت تماماً، وهذا يمكن أن يسبب مشاكل في الـ Reflection أو عند محاولة الوصول إلى الخصائص الداخلية للدالة.
import time
import functools
def timing(func):
@functools.wraps(func) # يحافظ على metadata الدالة الأصلية
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
@timing
def process_data(n):
return [x * 2 for x in range(n)]
# ما يحدث خلف الكواليس:
# process_data = timing(process_data)
# process_data(1000000) # يستدعي wrapper بدلاً من الدالة الأصليةأحد أقوى ميزات الديكوريترز هو قدرتها على الاحتفاظ بالحالة بين الاستدعاءات المختلفة. هذا ممكن بفضل مفهوم الـ Closure في بايثون. عندما تعيد الديكوريتر دالة داخلية (مثل wrapper في المثال السابق)، فإن هذه الدالة الداخلية تحتفظ بالبيئة المحيطة بها، بما في ذلك المتغيرات المحلية في الديكوريتر. لكن هنا تكمن المشكلة: إذا استخدمت متغيراً محلياً في الديكوريتر دون الإعلان عنه كـ nonlocal، فستجد نفسك أمام سلوك غير متوقع تماماً.
لنأخذ مثالاً متقدماً: ديكوريتر يحسب عدد مرات استدعاء الدالة. إذا كتبت الكود بدون nonlocal، فستجد أن العداد لا يزيد أبداً. السبب؟ المتغير counter داخل الدالة الداخلية wrapper يعتبر متغيراً محلياً جديداً في كل استدعاء، وليس المرجع الذي تريده. الحل؟ استخدام nonlocal لإخبار بايثون أن هذا المتغير يجب أن يشير إلى المتغير في النطاق الخارجي.
def call_counter(func):
counter = 0
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal counter # بدون هذا السطر، العداد لن يعمل أبداً
counter += 1
print(f"Function {func.__name__} called {counter} times")
return func(*args, **kwargs)
return wrapper
@call_counter
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # Function greet called 1 times
print(greet("Bob")) # Function greet called 2 timesالديكوريترز البسيطة سهلة الفهم، لكن عندما تحتاج إلى تمرير وسائط للديكوريتر نفسه، يتحول الأمر إلى لغز محير. هذا ليس مجرد إضافة قوسين حول الديكوريتر، بل يتطلب مستوى إضافياً من الـ Nesting. لماذا؟ لأن الديكوريتر الذي يقبل وسائط يجب أن يعيد دالة أخرى هي التي ستقبل الدالة الأصلية كوسيطة. هذا يعني أننا نتعامل مع ثلاث مستويات من الدوال: الديكوريتر الخارجي الذي يقبل الوسائط، والدالة الداخلية التي تقبل الدالة الأصلية، والدالة المغلفة التي ستُستدعى في النهاية.
لنأخذ مثالاً من العالم الحقيقي: مكتبة FastAPI تستخدم ديكوريترز مثل @app.get("/items/{item_id}") لقبول مسارات الـ API. خلف الكواليس، هذه ليست مجرد ديكوريتر بسيطة، بل هي ديكوريترز مع وسائط معقدة. عندما تكتب @app.get("/path")، فإن app.get هو دالة تعيد ديكوريتراً يقبل الدالة الأصلية كوسيطة. هذا المستوى الإضافي من التعقيد هو ما يسمح للمكتبة بتسجيل المسارات ومعالجتها بشكل ديناميكي.
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(3)
def say_hello(name):
return f"Hello, {name}!"
print(say_hello("Alice")) # ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
# ما يحدث خلف الكواليس:
# say_hello = repeat(3)(say_hello)في بيئات الإنتاج، يمكن أن تتحول الديكوريترز من حل أنيق إلى كابوس حقيقي إذا لم تُستخدم بحذر. أحد أكبر المشاكل هي الـ Performance Overhead. كل ديكوريتر تضيفه فوق دالة يضيف طبقة إضافية من الاستدعاءات، وهذا يمكن أن يكون كارثياً في الكود الحرج مثل الـ Inner Loops أو الـ Real-time Systems. مثلاً، في مشروع سابق، كان لدينا دالة معالجة صور تستخدم ديكوريترز لتسجيل الوقت والتحقق من الصلاحيات. بعد تحليل الأداء، اكتشفنا أن الديكوريترز كانت مسؤولة عن 40% من زمن التنفيذ الكلي!
مشكلة أخرى شائعة هي الـ Debugging. عندما يفشل الكود، ستجد أن الـ Stack Trace مليء بدوال wrapper بدلاً من الدوال الأصلية، وهذا يجعل تتبع الخطأ صعباً للغاية. الحل؟ استخدام functools.wraps للحفاظ على metadata الدالة الأصلية، واستخدام أدوات مثل sys.settrace لتتبع الاستدعاءات بدقة. أيضاً، يجب تجنب الديكوريترز التي تقوم بعمليات I/O داخلها، لأنها ستحول الكود من CPU-bound إلى I/O-bound، مما يسبب تجمد الـ Event Loop في التطبيقات غير المتزامنة.
الديكوريترز ليست مجرد أداة لتعديل سلوك الدوال، بل يمكن استخدامها لحل مشاكل معقدة مثل الـ Caching، والتحقق من الصلاحيات، وحتى إدارة الـ Concurrency. أحد أقوى الاستخدامات هو ديكوريتر الـ Memoization، الذي يخزن نتائج الدوال المكلفة لحسابها لتجنب إعادة الحساب. هذا مفيد بشكل خاص في الدوال الرياضية المتكررة أو الـ Recursive Functions مثل حساب الأعداد فيبوناتشي.
في عالم الـ Async، تصبح الديكوريترز أكثر قوة وتعقيداً. يمكنك كتابة ديكوريترز تعمل مع الدوال غير المتزامنة باستخدام async def، وهذا يسمح لك بإضافة سلوكيات مثل الـ Retry Mechanism أو الـ Rate Limiting للدوال التي تستدعي APIs خارجية. لكن هنا تكمن المشكلة: إذا كتبت ديكوريتر غير متزامن فوق دالة متزامنة، فستحصل على خطأ في وقت التشغيل. الحل؟ استخدام أدوات مثل inspect.iscoroutinefunction للتحقق من نوع الدالة قبل تطبيق الديكوريتر.
import functools
import asyncio
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(30)) # سريع جداً بفضل الـ Caching
# ديكوريتر غير متزامن
def retry(max_retries=3):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(1 * (attempt + 1))
return wrapper
return decorator
@retry(max_retries=3)
async def fetch_data(url):
# محاكاة استدعاء API قد يفشل
import random
if random.random() < 0.7:
raise ConnectionError("Failed to connect")
return {"data": "success"}إذا كنت تعمل مع مكتبات مثل FastAPI أو Django، فستجد أن الديكوريترز هي جزء لا يتجزأ من بنيتها. في FastAPI، الديكوريترز مثل @app.get و@app.post ليست مجرد اختصارات، بل هي جزء من نظام الـ Dependency Injection المعقد. عندما تكتب @app.get("/items/{item_id}")، فإن FastAPI تستخدم الديكوريتر لتسجيل المسار، تحليل الـ Parameters، والتحقق من أنواع البيانات تلقائياً. هذا يعني أنك إذا أخطأت في كتابة الديكوريتر، فقد لا يظهر الخطأ إلا في وقت التشغيل عند محاولة الوصول إلى المسار.
في Django، الديكوريترز مثل @login_required و@permission_required هي جزء أساسي من نظام التحقق من الصلاحيات. لكن المشكلة هنا هي أن هذه الديكوريترز تعمل على مستوى الـ View Functions فقط، وإذا أردت تطبيق نفس المنطق على مستوى الـ Class-based Views، فستحتاج إلى استخدام أدوات مثل method_decorator. أيضاً، يجب أن تكون حذراً عند دمج ديكوريترز متعددة، لأن ترتيب الديكوريترز مهم جداً. مثلاً، إذا وضعت @login_required فوق @cache_page، فسيتم التحقق من الصلاحيات قبل التخزين المؤقت، وهذا قد لا يكون السلوك الذي تريده.
# مثال من FastAPI
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
app = FastAPI()
def verify_token(token: str):
if token != "secret":
raise HTTPException(status_code=401, detail="Invalid token")
return True
@app.get("/protected/")
async def protected_route(token: str = Depends(verify_token)):
return {"message": "Access granted"}
# مثال من Django
from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views import View
class MyView(View):
@method_decorator(login_required)
@method_decorator(permission_required('app.view_data'))
@method_decorator(cache_page(60 * 15))
def get(self, request):
return HttpResponse("Protected and cached content")إذا أخذت شيئاً واحداً من هذا المقال، فليكن هذا: الديكوريترز ليست مجرد سكر تركيبي، بل هي نمط برمجي متكامل يجب أن تفهمه بعمق قبل استخدامه. ابدأ دائماً بكتابة الديكوريترز البسيطة بدون وسائط، ثم انتقل إلى الديكوريترز المعقدة فقط عندما تكون هناك حاجة حقيقية. استخدم functools.wraps دائماً، واختبر أداء الديكوريترز بشكل منفصل قبل دمجها في الكود الحرج. وإذا وجدت نفسك تكتب ديكوريترز متداخلة أكثر من مستويين، فاعلم أنك إما تقوم بشيء خاطئ جداً، أو أنك بحاجة إلى إعادة تصميم الكود بالكامل. الديكوريترز القوية هي تلك التي تجعل الكود أكثر وضوحاً وقابلية للصيانة، وليس العكس.