اكتشف كيف تحولDecorators في بايثون الكود من تكرار ممل إلى فن برمجي، مع أمثلة عملية تتجاوز الأساسيات لتصل إلى تقنيات تستخدمها شركات مثل جوجل وريديت في الإنتاج.
تخيل أنك تعمل على واجهة برمجية لمعالجة الصور في شركة ناشئة، وكل مكالمة لـ API تحتاج إلى تسجيل الوقت، التحقق من الصلاحيات، وتخزين النتيجة في قاعدة البيانات. تكتب نفس الكود ثلاث مرات، ثم تكتشف أن أحد المطورين نسى إضافة التحقق في نقطة واحدة. النتيجة؟ ثغرة أمنية تسمح للمستخدمين غير المصرح لهم بمعالجة صور حساسة. هنا بالضبط تظهر قوة Python Decorators - إنها ليست مجرد أداة لتنظيم الكود، بل هي طريقة لإعادة التفكير في كيفية بناء الوظائف نفسها.
في عالم بايثون، الـ Decorators هي أكثر من مجرد سكر نحوي. إنها آلية تسمح لك بتعديل سلوك الوظائف أو الكلاسات دون تغيير كودها الأصلي. فكر فيها كطبقة شفافة تلتف حول الوظيفة الأصلية، تضيف إليها سلوكاً جديداً دون أن تلمس محتواها. هذا المفهوم ليس مجرد ترفيهي - شركات مثل ريديت تستخدم Decorators لإدارة الـ rate limiting في واجهات برمجتها، بينما تستخدمها جوجل في مكتبات مثل TensorFlow لإضافة سلوكيات مثل الـ logging والتحقق من المدخلات دون تكرار الكود.
عندما تكتب @retry فوق وظيفة، بايثون لا تضيف مجرد سطر جميل فوق التعريف. إنها تنفذ عملية معقدة في الذاكرة. أولاً، تُنشئ بايثون كائناً جديداً من نوع function يمثل الـ decorator نفسه. ثم تمرر الوظيفة الأصلية (التي تريد تزيينها) كوسيط لهذا الكائن. داخل الـ decorator، تُنشئ بايثون وظيفة جديدة مغلفة (wrapper) تلتقط كلاً من الـ arguments والـ keyword arguments الأصلية. هذا الـ wrapper هو الذي سيُستدعى فعلياً عند استدعاء الوظيفة المزينة، وليس الوظيفة الأصلية.
المثير للاهتمام هنا هو أن بايثون تحتفظ بمرجع للوظيفة الأصلية عبر closure. هذا يعني أن الـ wrapper يمكنها الوصول إلى المتغيرات المحلية في نطاق الـ decorator حتى بعد انتهاء تنفيذه. إذا كنت تستخدم متغيرات مثل max_retries داخل الـ decorator، فسيتم الاحتفاظ بقيمتها في الذاكرة طالما أن الوظيفة المزينة موجودة. هذا السلوك هو ما يسمح للـ Decorators بأن تكون ديناميكية وقابلة لإعادة الاستخدام في نفس الوقت، لكنه أيضاً ما قد يسبب تسريبات للذاكرة إذا لم تكن حذراً مع المتغيرات التي تلتقطها الـ closures.
import time
from functools import wraps
def log_execution_time(func):
@wraps(func) # يحافظ على metadata الوظيفة الأصلية
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs) # استدعاء الوظيفة الأصلية
end_time = time.perf_counter()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@log_execution_time
def process_large_dataset(data):
# محاكاة معالجة بيانات تستغرق وقتاً
time.sleep(2)
return sum(x * 2 for x in data if x > 100)
# عند الاستدعاء، wrapper هي التي تُستدعى بالفعل
result = process_large_dataset(range(10000))في المشاريع الكبيرة، غالباً ما تحتاج إلى تطبيق أكثر من decorator على نفس الوظيفة. مثلاً، قد تحتاج إلى تسجيل الوقت، التحقق من الصلاحيات، والتعامل مع الأخطاء في نفس الوقت. هنا تأتي قوة الـ nested decorators. بايثون تطبق الـ decorators من الأسفل إلى الأعلى، لكن التنفيذ الفعلي يحدث من الأعلى إلى الأسفل. هذا يعني أن الـ decorator الأقرب إلى تعريف الوظيفة هو الأول في التنفيذ، والأبعد هو الأخير.
المشكلة الشائعة هنا هي فقدان metadata الوظيفة الأصلية. عندما تطبق عدة decorators، قد ينتهي بك الأمر بوظيفة لا تعرف اسمها أو docstring الأصلي. هذا هو سبب أهمية استخدام @wraps من وحدة functools - فهي تحافظ على هوية الوظيفة الأصلية حتى بعد تطبيق عدة طبقات من الـ decorators. بدونها، ستجد أن debuggers وأدوات مثل help() ستبدأ بإظهار معلومات خاطئة عن وظيفتك، مما يجعل عملية التطوير أكثر صعوبة بكثير.
from functools import wraps
def authorize(role_required):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_role = kwargs.get('user_role', 'guest')
if user_role != role_required:
raise PermissionError(f"Role {role_required} required")
return func(*args, **kwargs)
return wrapper
return decorator
def cache_response(timeout=60):
cache = {}
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = (args, frozenset(kwargs.items()))
if cache_key in cache:
result, timestamp = cache[cache_key]
if time.time() - timestamp < timeout:
return result
result = func(*args, **kwargs)
cache[cache_key] = (result, time.time())
return result
return wrapper
return decorator
@cache_response(timeout=300)
@authorize('admin')
def get_sensitive_data(user_id, user_role='guest'):
"""Retrieve sensitive data from database"""
print(f"Fetching data for user {user_id}")
return {"data": f"Sensitive info for {user_id}"}
# التنفيذ: cache_response -> authorize -> الوظيفة الأصلية
# لكن عند الاستدعاء: authorize أولاً ثم cache_responseالعديد من المطورين يعتقدون أن الـ Decorators تقتصر على الوظائف، لكن الحقيقة هي أنها تعمل بنفس الفعالية مع الكلاسات والطرق. عندما تزين طريقة في كلاس، فإن الـ decorator يتلقى self كوسيط أول، تماماً مثل أي طريقة عادية. هذا يعني أنه يمكنك تعديل سلوك الطرق بناءً على حالة الكائن، أو حتى إضافة طرق جديدة ديناميكياً إلى الكلاس.
المثال الكلاسيكي هنا هو إضافة سلوكيات مثل الـ logging أو الـ validation إلى الطرق دون تغيير الكود الأصلي. لكن الاستخدامات الأكثر تقدماً تشمل تعديل سلوك الكلاس بالكامل بناءً على ظروف التشغيل. مثلاً، يمكنك استخدام decorator لتحويل كلاس عادي إلى كلاس singleton بضربة واحدة، أو لإضافة طرق جديدة إلى الكلاس بناءً على الإعدادات في وقت التشغيل. هذا النوع من المرونة هو ما يجعل بايثون لغة مثالية للمشاريع التي تحتاج إلى تعديل سلوكها ديناميكياً دون إعادة كتابة الكود.
def singleton(cls):
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
def get_db_connection():
print("Creating new DB connection")
return {"connection": "live"}
# كل استدعاء يرجع نفس الكائن
c get_db_connection()
conn2 = get_db_connection()
print(conn1 is conn2) # True
# مع الكلاسات
@singleton
class Database:
def __init__(self, url):
self.url = url
print(f"Initializing database at {url}")
# نفس الكائن يُعاد في كل مرة
db1 = Database("postgres://localhost")
db2 = Database("postgres://another")
print(db1 is db2) # True
print(db1.url) # postgres://localhostعندما تجمع بين الـ Decorators و الـ Descriptors، تحصل على قوة هائلة في التحكم في سلوك الكلاسات. الـ Descriptors هي آلية بايثون تسمح لك بتخصيص ما يحدث عند الوصول إلى سمات الكلاس (get, set, delete). عندما تستخدم decorator لتعديل طريقة تعمل كـ descriptor، يمكنك تغيير سلوك الوصول إلى السمات ديناميكياً بناءً على ظروف معينة.
المثال العملي هنا هو إضافة سلوكيات مثل الـ lazy loading أو الـ caching إلى سمات الكلاس. بدلاً من تحميل البيانات الثقيلة عند إنشاء الكائن، يمكنك استخدام decorator لجعل السمة تُحمل فقط عند أول وصول إليها. هذا النوع من الأنماط يُستخدم بكثرة في أطر العمل مثل Django و SQLAlchemy لتحسين أداء التطبيقات الكبيرة.
class LazyProperty:
def __init__(self, func):
self.func = func
self.attr_name = f"_{func.__name__}"
def __get__(self, instance, owner):
if instance is None:
return self
if not hasattr(instance, self.attr_name):
setattr(instance, self.attr_name, self.func(instance))
return getattr(instance, self.attr_name)
def lazy_load(func):
return LazyProperty(func)
class UserProfile:
def __init__(self, user_id):
self.user_id = user_id
@lazy_load
def posts(self):
print("Loading posts from database...")
return [f"Post {i}" for i in range(100)]
@lazy_load
def friends(self):
print("Loading friends from API...")
return [f"Friend {i}" for i in range(50)]
profile = UserProfile(1)
print("Profile created")
# لم تُحمل البيانات بعد
print(profile.posts[0]) # الآن تُحمل البيانات
print(profile.friends) # تُحمل بيانات الأصدقاءعندما تنتقل من الأمثلة البسيطة إلى الاستخدام في الإنتاج، ستواجه مشاكل حقيقية لا تغطيها معظم الدروس. أحد أكبر هذه المشاكل هو تسرب الذاكرة بسبب الـ closures. عندما يلتقط الـ decorator متغيرات من النطاق الخارجي، تبقى هذه المتغيرات في الذاكرة طالما أن الوظيفة المزينة موجودة. إذا كنت تستخدم متغيرات كبيرة داخل الـ decorator، فقد ينتهي بك الأمر باستهلاك ذاكرة هائلة دون أن تدري.
مشكلة أخرى شائعة هي الأداء. كل decorator تضيفه يضيف طبقة إضافية من الاستدعاءات، وهذا يعني زيادة في وقت التنفيذ. في التطبيقات الحساسة للأداء، قد تلاحظ أن الـ decorators تضيف تكلفة غير مقبولة. الحل هنا هو استخدام أدوات مثل cProfile لقياس التأثير الحقيقي، وأحياناً استبدال الـ decorators البسيطة بكود مباشر في النقاط الحرجة من التطبيق.
إحدى الاستخدامات العملية الأكثر شيوعاً للـ Decorators في الشركات الكبيرة هي إدارة الـ rate limiting في واجهات البرمجة. ريديت، على سبيل المثال، تستخدم نظاماً مشابهاً للتحكم في عدد الطلبات التي يمكن للمستخدم إرسالها في فترة زمنية معينة. الفكرة بسيطة: بدلاً من كتابة كود التحقق في كل نقطة نهاية، تستخدم decorator واحدة تُطبق على جميع الـ endpoints التي تحتاج إلى هذا السلوك.
الميزة الحقيقية هنا هي المرونة. يمكنك تعديل سلوك الـ rate limiting في مكان واحد دون الحاجة إلى تغيير كل نقطة نهاية. بالإضافة إلى ذلك، يمكنك تطبيق مستويات مختلفة من القيود بناءً على نوع المستخدم أو نوع الطلب. مثلاً، المستخدمون العاديون قد يكون لديهم حد 60 طلب في الدقيقة، بينما المستخدمون المدفوعون قد يحصلون على 1000 طلب في الدقيقة. كل هذا يمكن التحكم فيه من خلال وسيط يُمرر للـ decorator عند تعريفه.
import time
from functools import wraps
from collections import defaultdict
# تخزين عدد الطلبات لكل مستخدم
request_counts = defaultdict(list)
def rate_limit(max_requests, period):
def decorator(func):
@wraps(func)
def wrapper(user_id, *args, **kwargs):
now = time.time()
# إزالة الطلبات القديمة
request_counts[user_id] = [
t for t in request_counts[user_id]
if now - t < period
]
if len(request_counts[user_id]) >= max_requests:
oldest_request = request_counts[user_id][0]
wait_time = period - (now - oldest_request)
raise Exception(f"Rate limit exceeded. Try again in {wait_time:.1f} seconds")
request_counts[user_id].append(now)
return func(user_id, *args, **kwargs)
return wrapper
return decorator
@rate_limit(max_requests=5, period=60)
def get_user_posts(user_id):
"""Get posts for a specific user"""
return [f"Post {i} by {user_id}" for i in range(10)]
# اختبار الـ rate limiting
for i in range(6):
try:
print(f"Request {i+1}: {get_user_posts('user123')[:2]}...")
except Exception as e:
print(f"Request {i+1}: {e}")على الرغم من قوة الـ Decorators، هناك حالات يجب فيها تجنب استخدامها تماماً. أول هذه الحالات هي عندما يكون الكود حساساً للأداء بشكل كبير. كل decorator تضيفه يضيف طبقة إضافية من الاستدعاءات، وهذا يعني زيادة في وقت التنفيذ. في التطبيقات التي تحتاج إلى استجابة فورية مثل الألعاب أو أنظمة التداول عالية التردد، قد تكون هذه التكلفة غير مقبولة.
الحالة الثانية هي عندما يكون الكود بحاجة إلى شفافية كاملة. الـ Decorators تجعل سلوك الوظائف أقل وضوحاً، خاصة للمطورين الجدد في الفريق. إذا كان عليك شرح كيفية عمل وظيفة معينة لعدة دقائق، فقد يكون من الأفضل كتابة الكود بشكل مباشر بدلاً من استخدام decorator معقد. تذكر أن الهدف من البرمجة هو التواصل مع البشر أولاً، والآلات ثانياً.
أخيراً، تجنب استخدام الـ Decorators عندما يكون السلوك الذي تريد إضافته فريداً جداً ولا يُستخدم في أماكن أخرى. إذا كنت تكتب decorator يُستخدم مرة واحدة فقط، فأنت تضيف تعقيداً دون أي فائدة حقيقية. في هذه الحالة، يكون من الأفضل كتابة الكود مباشرة داخل الوظيفة نفسها. القاعدة البسيطة هنا هي: إذا لم يكن الكود قابلاً لإعادة الاستخدام في ثلاثة أماكن مختلفة على الأقل، فلا تجعله decorator.
الـ Decorators في بايثون ليست مجرد أداة لتنظيم الكود - إنها طريقة لإعادة التفكير في كيفية بناء البرامج. عندما تفهم حقاً كيف تعمل الـ closures و الـ first-class functions، ستدرك أن الوظائف في بايثون ليست مجرد كتل من الكود، بل هي كائنات حية يمكن تعديلها وتوسيعها ديناميكياً. هذه العقلية هي ما يميز المطورين الذين يكتبون كوداً نظيفاً وقابلاً للصيانة عن أولئك الذين يكتبون كوداً يعمل فحسب.
نصيحة عملية أخيرة: ابدأ دائماً بكتابة الـ decorator كدالة عادية تستقبل الوظيفة كوسيط، ثم حولها إلى صيغة @ بعد أن تتأكد من أنها تعمل بشكل صحيح. هذه الطريقة تجعل عملية الـ debugging أسهل بكثير، وتجنبك الكثير من الصداع عندما لا يعمل الكود كما تتوقع. وعندما تصبح مرتاحاً مع الأساسيات، انتقل إلى الاستخدامات المتقدمة مثل الـ nested decorators والـ decorators مع الكلاسات. بهذه الطريقة، ستتحول من مطور يستخدم الـ decorators إلى مهندس يفهم حقاً كيف تعمل بايثون تحت الغطاء.