اكتشف كيف تحول الديكوريترز في بايثون من مفهوم غامض إلى أداة قوية تحل مشاكل حقيقية في الإنتاج، مع أمثلة متقدمة تكشف ما يحدث خلف الكواليس في الذاكرة والمعالج.
كنت أعمل على مشروع ضخم لإحدى شركات التجارة الإلكترونية الكبرى، وكان لدينا نظام تسجيل دخول معقد يعتمد على عدة طبقات من المصادقة. فجأة، لاحظنا أن السيرفر بدأ يعلق بشكل عشوائي عند كل طلب تسجيل دخول. بعد ساعات من التنقيح، اكتشفنا أن المشكلة تكمن في دالة بسيطة كانت تتكرر في 12 مكاناً مختلفاً في الكود، وكل مرة كانت تضيف 150 ميلي ثانية من التأخير بسبب استدعاء غير متزامن لـ API خارجي. هنا أدركت أن الديكوريترز ليست مجرد زينة لغوية، بل أداة هندسية حقيقية قادرة على حل مشاكل الإنتاج بفعالية مذهلة.
في بايثون، الديكوريترز هي واحدة من تلك الميزات التي تبدو سحرية في البداية، لكن عندما تفهم كيف تعمل خلف الكواليس، ستجد نفسك تستخدمها يومياً لحل مشاكل حقيقية. المشكلة أن معظم الشروحات تتوقف عند الأمثلة البسيطة مثل @timing أو @log، بينما في الواقع، الديكوريترز قادرة على التعامل مع سيناريوهات معقدة مثل التحكم في الوصول، إدارة الـ Context، وحتى تعديل سلوك الدوال ديناميكياً أثناء التشغيل. دعنا نكسر الحاجز بين الفهم النظري والاستخدام الاحترافي.
عندما تكتب @decorator فوق دالة، بايثون لا تقوم ببساطة بتعديل الدالة الأصلية. بدلاً من ذلك، يحدث شيء أكثر ذكاءً: بايثون تنشئ دالة جديدة (الديكوريتر) تأخذ الدالة الأصلية كوسيط، ثم تعيد دالة مغلفة تحل محل الدالة الأصلية في جدول الرموز. هذا يعني أن الدالة الأصلية تبقى موجودة في الذاكرة، لكن اسمها الآن يشير إلى النسخة المغلفة. هذا السلوك له تبعات مهمة على استهلاك الذاكرة وأدائها، خاصة في التطبيقات الكبيرة.
لنأخذ مثالاً عملياً: عندما تستخدم @lru_cache من مكتبة functools، بايثون لا تقوم فقط بتخزين النتائج في قاموس بسيط. خلف الكواليس، هناك آلية متكاملة تتعامل مع الـ Hashing، إدارة الذاكرة، وتنظيف الـ Cache تلقائياً عندما يصل إلى الحد المحدد. هذا هو السبب في أن الديكوريترز مثل @lru_cache يمكنها تحسين أداء التطبيقات بشكل كبير دون الحاجة لتعديل الكود الأصلي. لكن هناك فخ هنا: إذا استخدمت @lru_cache على دالة تتعامل مع مدخلات غير قابلة للتجزئة (unhashable types)، ستحصل على خطأ في وقت التشغيل، وهذا خطأ شائع في الإنتاج.
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_calculation(n):
# محاكاة عملية حسابية مكلفة
return sum(i * i for i in range(n))
# أول استدعاء: الحساب الفعلي
print(expensive_calculation(10000)) # 333383335000
# استدعاء ثانٍ بنفس المدخلات: يعود النتيجة من الـ Cache
print(expensive_calculation(10000)) # نفس النتيجة، لكن أسرع بكثير
# فحص عدد مرات الاستدعاء الفعلي
print(expensive_calculation.cache_info()) # CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)في المشاريع الحقيقية، نادراً ما تستخدم ديكوريتر واحدة فقط. عادةً ما تجد نفسك تستخدم عدة ديكوريترز فوق نفس الدالة، وهذا هو المكان الذي تصبح فيه الأمور مثيرة للاهتمام. بايثون تطبق الديكوريترز من الأعلى إلى الأسفل، لكن التنفيذ الفعلي يحدث من الداخل إلى الخارج. هذا يعني أن الديكوريتر الأقرب إلى الدالة الأصلية هو الذي ينفذ أولاً، ثم يأتي الدور على الديكوريتر التالي، وهكذا. هذا السلوك يمكن أن يكون مفيداً جداً، لكنه أيضاً مصدر للأخطاء إذا لم تفهمه جيداً.
لنأخذ سيناريو واقعي: لديك دالة تقوم بإرسال إشعارات للمستخدمين، وتريد إضافة ثلاث طبقات من التحكم: تسجيل الدخول، التحقق من الصلاحيات، وتحديد معدل الطلبات (Rate Limiting). إذا استخدمت الديكوريترز بترتيب خاطئ، قد ينتهي بك الأمر بتسجيل الدخول قبل التحقق من الصلاحيات، وهذا خطأ أمني خطير. الحل هو فهم ترتيب التنفيذ جيداً واختبار كل سيناريو بعناية. في تجربتي، أفضل طريقة لتجنب هذه الأخطاء هي كتابة اختبارات وحدة لكل ديكوريتر على حدة، ثم اختبارها معاً.
import time
from functools import wraps
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
def check_permissions(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.is_admin:
raise PermissionError("User does not have admin privileges")
return func(user, *args, **kwargs)
return wrapper
def rate_limit(max_calls, period):
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
calls_in_period = [call for call in calls if now - call < period]
if len(calls_in_period) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
# تطبيق الديكوريترز بترتيب محدد
@log_execution
@check_permissions
@rate_limit(max_calls=3, period=60)
def send_notification(user, message):
print(f"Sending notification to {user.name}: {message}")
time.sleep(0.5) # محاكاة تأخير الشبكة
class User:
def __init__(self, name, is_admin=False):
self.name = name
self.is_admin = is_admin
admin_user = User("Admin", is_admin=True)
regular_user = User("User")
# سيناريو صحيح
send_notification(admin_user, "System update available")
# سيناريو خاطئ: المستخدم ليس أدمن
try:
send_notification(regular_user, "You have a new message")
except PermissionError as e:
print(f"Error: {e}")
# سيناريو خاطئ: تجاوز حد الطلبات
for i in range(4):
try:
send_notification(admin_user, f"Message {i+1}")
except Exception as e:
print(f"Error: {e}")في بعض الحالات، تحتاج إلى أن يكون سلوك الديكوريتر قابلاً للتعديل بناءً على مدخلات معينة. مثلاً، قد تريد تحديد مستوى التسجيل (Debug, Info, Error) ديناميكياً، أو تحديد مدة الـ Timeout بناءً على نوع الطلب. هنا تأتي الديكوريترز مع الباراميترات، وهي تقنية قوية لكنها غالباً ما تُساء فهمها. الفكرة الأساسية هي أن الديكوريتر نفسها تصبح دالة تأخذ الباراميترات، ثم تعيد دالة ديكوريتر فعلية.
المشكلة الشائعة هنا هي نسيان أن الديكوريتر مع الباراميترات يتطلب ثلاث مستويات من الدوال المتداخلة: الدالة الخارجية التي تأخذ الباراميترات، الدالة الوسطى التي هي الديكوريتر الفعلية، والدالة الداخلية التي تغلف الدالة الأصلية. هذا التعقيد يمكن أن يؤدي إلى أخطاء في إدارة الـ Scope، خاصة عندما تتعامل مع متغيرات غير محلية (nonlocal variables). في أحد المشاريع، واجهنا مشكلة غريبة حيث كانت قيمة متغير غير محلي تتغير بشكل غير متوقع بين الاستدعاءات، واكتشفنا لاحقاً أن السبب هو أننا لم نستخدم nonlocal بشكل صحيح داخل الديكوريتر.
import logging
from functools import wraps
def log_with_level(level=logging.INFO):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger = logging.getLogger(func.__module__)
logger.log(level, f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
try:
result = func(*args, **kwargs)
logger.log(level, f"{func.__name__} returned: {result}")
return result
except Exception as e:
logger.log(logging.ERROR, f"{func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
return decorator
# إعداد التسجيل
logging.basicConfig(level=logging.DEBUG)
@log_with_level(logging.DEBUG)
def divide(a, b):
return a / b
# سيناريو صحيح
divide(10, 2)
# سيناريو خاطئ
try:
divide(10, 0)
except ZeroDivisionError:
print("Caught division by zero")عندما تنتقل من الأمثلة البسيطة إلى الاستخدام في الإنتاج، ستواجه مجموعة من التحديات التي لا تغطيها معظم الشروحات. أحد أكبر هذه التحديات هو إدارة الـ State داخل الديكوريترز. مثلاً، إذا كان لديك ديكوريتر يحتفظ بحالة (مثل عدد الاستدعاءات)، فأنت بحاجة إلى التفكير في كيفية مشاركة هذه الحالة بين العمليات المختلفة في بيئة متعددة الخيوط أو متعددة العمليات. في بايثون، المتغيرات العادية داخل الديكوريترز تكون مشتركة بين جميع الاستدعاءات في نفس العملية، وهذا يمكن أن يؤدي إلى مشاكل في التطبيقات متعددة الخيوط.
مشكلة أخرى شائعة هي فقدان الـ Metadata للدالة الأصلية. عندما تستخدم الديكوريترز، بايثون تستبدل الدالة الأصلية بالدالة المغلفة، وهذا يعني أن معلومات مثل اسم الدالة، الـ Docstring، وحتى الـ Annotations يمكن أن تضيع. هذا ليس مجرد مشكلة تجميلية - يمكن أن يؤثر على أدوات مثل الـ Debuggers، الـ Profilers، وحتى مكتبات مثل FastAPI التي تعتمد على الـ Metadata لإنشاء الوثائق تلقائياً. الحل هو استخدام @wraps من مكتبة functools، وهي ديكوريتر بسيطة لكنها ضرورية للحفاظ على الـ Metadata الأصلية.
from functools import wraps
import threading
import time
def thread_safe_counter(func):
count = 0
lock = threading.Lock()
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
with lock:
count += 1
print(f"Function {func.__name__} called {count} times")
return func(*args, **kwargs)
return wrapper
@thread_safe_counter
def process_data(data):
"""Processes the given data and returns the result."""
time.sleep(0.1) # محاكاة معالجة البيانات
return f"Processed: {data}"
# اختبار في بيئة متعددة الخيوط
import concurrent.futures
def worker(data):
return process_data(data)
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(worker, i) for i in range(10)]
results = [f.result() for f in futures]
print(results)عندما تتقن الأساسيات، يمكنك البدء في استكشاف إمكانيات أكثر تقدماً للديكوريترز. مثلاً، يمكنك إنشاء ديكوريترز ديناميكية تتغير سلوكها بناءً على ظروف معينة أثناء التشغيل. هذا مفيد جداً في سيناريوهات مثل A/B Testing، حيث تريد تغيير سلوك الدالة بناءً على مجموعة من المستخدمين. في أحد المشاريع، استخدمنا هذه التقنية لتغيير خوارزمية التوصيات ديناميكياً بناءً على سلوك المستخدم دون الحاجة لإعادة نشر الكود.
تقنية أخرى قوية هي استخدام الديكوريترز لإنشاء ما يسمى بـ "Context Managers" ديناميكياً. بدلاً من استخدام with statement بشكل صريح، يمكنك إنشاء ديكوريتر يقوم تلقائياً بإدارة الـ Context قبل وبعد استدعاء الدالة. هذا مفيد جداً في سيناريوهات مثل إدارة قواعد البيانات، حيث تريد التأكد من إغلاق الاتصال دائماً حتى لو حدثت استثناءات. في تجربتي، هذه التقنية قللت من عدد الأخطاء المتعلقة بعدم إغلاق الموارد بشكل كبير.
from functools import wraps
import random
def dynamic_behavior(behavior_map):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# اختيار السلوك بناءً على شرط معين
user_id = kwargs.get('user_id', 0)
behavior = behavior_map.get(user_id % len(behavior_map), behavior_map[0])
print(f"Using behavior: {behavior.__name__} for user {user_id}")
return behavior(*args, **kwargs)
return wrapper
return decorator
def recommendation_algorithm_a(items):
return sorted(items, key=lambda x: random.random())
def recommendation_algorithm_b(items):
return sorted(items, key=lambda x: -x['rating'])
# تعيين السلوكيات المختلفة للمستخدمين
behavior_map = {
0: recommendation_algorithm_a,
1: recommendation_algorithm_b
}
@dynamic_behavior(behavior_map)
def get_recommendations(user_id, items):
"""Returns recommended items for the given user."""
return items
# اختبار
items = [
{"id": 1, "rating": 4.5},
{"id": 2, "rating": 3.8},
{"id": 3, "rating": 4.9}
]
print(get_recommendations(user_id=100, items=items)) # يستخدم algorithm_a
print(get_recommendations(user_id=101, items=items)) # يستخدم algorithm_bإذا كنت تريد أن ترى كيف تستخدم الديكوريترز في الإنتاج، فلا يوجد أفضل من النظر إلى المكتبات الشهيرة. مثلاً، مكتبة FastAPI تستخدم الديكوريترز بشكل مكثف لإنشاء الـ API Endpoints. عندما تكتب @app.get("/items/")، فأنت في الواقع تستخدم ديكوريتر يقوم بتسجيل الدالة كـ Endpoint، ويحلل الـ Type Hints لإنشاء وثائق تلقائية، ويتعامل مع الـ Request/Response تلقائياً. هذا الاستخدام يظهر كيف يمكن للديكوريترز أن تقلل من الكود المتكرر وتجعل الكود أكثر قابلية للصيانة.
مثال آخر هو مكتبة Celery، التي تستخدم الديكوريترز لتحديد المهام غير المتزامنة. عندما تكتب @app.task فوق دالة، Celery تنشئ نسخة من الدالة يمكن تشغيلها في بيئة منفصلة، وتتعامل مع جميع التفاصيل المعقدة مثل توزيع المهام، إعادة المحاولة عند الفشل، وتسجيل النتائج. هذا الاستخدام يظهر كيف يمكن للديكوريترز أن تخفي التعقيد وتوفر واجهة بسيطة للمطورين. الدرس هنا هو أن الديكوريترز ليست مجرد أداة لتحسين الكود، بل يمكنها أن تكون جزءاً أساسياً من بنية التطبيق.
# مثال مبسط لكيفية عمل ديكوريتر مثل @app.route في Flask
from functools import wraps
class SimpleRouter:
def __init__(self):
self.routes = {}
def route(self, path):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Handling request for {path}")
return func(*args, **kwargs)
self.routes[path] = wrapper
return wrapper
return decorator
def serve(self, path):
handler = self.routes.get(path)
if handler:
return handler()
else:
return "404 Not Found"
app = SimpleRouter()
@app.route("/")
def home():
return "Welcome to the home page!"
@app.route("/about")
def about():
return "About us page"
# محاكاة طلبات HTTP
print(app.serve("/")) # Welcome to the home page!
print(app.serve("/about")) # About us page
print(app.serve("/contact")) # 404 Not Foundبعد أكثر من عشر سنوات في استخدام الديكوريترز في مشاريع حقيقية، هذه هي النصائح التي أتمنى أن يعرفها كل مطور بايثون: أولاً، استخدم @wraps دائماً - إنها لا تستهلك موارد تذكر لكنها تحافظ على صحة الـ Metadata وتجعل الـ Debugging أسهل بكثير. ثانياً، تجنب الاحتفاظ بحالة داخل الديكوريترز إلا إذا كنت تعرف بالضبط ما تفعله، خاصة في بيئات متعددة الخيوط. ثالثاً، إذا وجدت نفسك تكتب نفس الديكوريتر أكثر من مرتين، فكر في تحويله إلى مكتبة أو على الأقل وحدة منفصلة يمكن إعادة استخدامها.
وأخيراً، تذكر أن الديكوريترز ليست حلاً سحرياً لكل مشكلة. في بعض الحالات، قد يكون استخدام الكلاسات أو حتى مجرد دالة مساعدة أبسط وأكثر وضوحاً. القاعدة الذهبية هي: إذا كان استخدام الديكوريتر سيجعل الكود أكثر تعقيداً بدلاً من تبسيطه، فلا تستخدمه. الديكوريترز هي أداة قوية، لكنها مثل أي أداة أخرى - قيمتها تأتي من استخدامها في المكان والوقت المناسبين.
الديكوريترز في بايثون هي مثل التوابل في الطبخ: القليل منها يضيف نكهة رائعة، لكن الكثير منها يمكن أن يفسد الطبق بالكامل.
— مطور بايثون مجهول