اكتشف كيف تحولDecorators في بايثون الدوال العادية إلى أدوات برمجية قوية تتحكم في الأداء، الأمان، والـ Logging دون تعديل الكود الأصلي. أمثلة عملية متقدمة تكشف أسرار ما يحدث خلف الكواليس في الذاكرة.
تخيل أنك تعمل على نظام دفع إلكتروني لمعالجته آلاف الطلبات في الثانية، وكل ميكرو ثانية إضافية في تنفيذ الدالة تعني خسارة مالية مباشرة. فجأة تكتشف أن كل دالة في النظام تضيف 200 ميلي ثانية إضافية بسبب فحص الصلاحيات وتسجيل الـ Logs، رغم أن الكود نفسه نظيف وفعال. المشكلة ليست في منطق الدالة، بل في الـ Boilerplate المتكرر الذي يلتف حولها. هنا تأتي الـ Decorators كأداة سحرية لا تضيف فقط نظافة للكود، بل تعيد تشكيل طريقة تفكيرك في البرمجة الوظيفية نفسها.
الـ Decorators في بايثون ليست مجرد ميزة لغوية جميلة، بل هي نمط تصميم متقدم (Design Pattern) يسمح لك بتعديل سلوك الدوال أو الكلاسات دون تغيير الكود الأصلي. فكر فيها كطبقة شفافة تلتف حول الدالة الأصلية، تضيف وظائف قبل وبعد تنفيذها، أو حتى تمنع تنفيذها تماماً بناءً على شروط معينة. لكن خلف هذه البساطة الظاهرة تكمن آليات معقدة في الذاكرة: الـ Closures، الـ First-Class Functions، و الـ Introspection التي تجعل بايثون لغة فريدة في التعامل مع الدوال ككائنات من الدرجة الأولى.
عندما تكتب @log_execution_time فوق دالة ما، بايثون لا تفسر هذا كـ "سحر" بل كتعليمات دقيقة تنفذ على مستوى الـ Bytecode. أولاً، الدالة الأصلية تُمرر كوسيط للدالة الزخرفية (Decorator Function)، التي تُنشئ دالة جديدة (Wrapper Function) تحتفظ بمرجع للدالة الأصلية عبر الـ Closure. هذا الـ Wrapper هو الذي سيُستدعى فعلياً عند استدعاء الدالة المزخرفة، وليس الدالة الأصلية نفسها. المشكلة التي لا يدركها الكثيرون هي أن هذا الـ Wrapper يحتفظ بمرجع للدالة الأصلية في الذاكرة، مما قد يسبب تسرب ذاكرة (Memory Leak) إذا احتوى الـ Decorator على كائنات كبيرة أو اتصالات خارجية دون إغلاقها بشكل صحيح.
لنأخذ مثالاً عملياً: دالة تحسب الفوائد البنكية. بدون زخرفة، الكود بسيط وفعال. لكن عند إضافة زخرفة لتسجيل وقت التنفيذ، بايثون تنفذ الخطوات التالية خلف الكواليس:
import time
def log_execution_time(func):
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 calculate_interest(principal, rate, years):
return principal * (1 + rate) ** years
# عند استدعاء calculate_interest(1000, 0.05, 10)
# بايثون تستدعي في الواقع wrapper(1000, 0.05, 10)لاحظ كيف أن الـ wrapper تحتفظ بمرجع للدالة الأصلية func عبر الـ Closure. هذا يعني أن كل مرة تستدعي فيها الدالة المزخرفة، بايثون تمر عبر هذه الطبقة الإضافية، مما يضيف تكلفة زمنية ضئيلة (حوالي 100-200 نانوثانية في اختباراتي) لكنها قد تكون ملحوظة في الأنظمة عالية الأداء. الحقيقة التي لا يتحدث عنها كثيرون هي أن هذه التكلفة ليست دائماً مبررة، خاصة في الدوال التي تُستدعى ملايين المرات داخل حلقات تكرارية ضيقة (Tight Loops).
في المشاريع الحقيقية، نادراً ما تستخدم زخرفة واحدة فقط. عادةً ما تجد دالة مزخرفة بعدة طبقات: واحدة لتسجيل الوقت، أخرى لفحص الصلاحيات، وثالثة للتخزين المؤقت (Caching). المشكلة هنا أن ترتيب الـ Decorators يهم كثيراً، وقد يؤدي إلى سلوك غير متوقع إذا لم تفهم كيف تُطبق الطبقات. القاعدة الذهبية هي: الزخرفة الأقرب للدالة تُنفذ أولاً، ثم تتحرك للخارج. لكن هذا ليس مجرد ترتيب تنفيذي، بل يؤثر أيضاً على كيفية احتفاظ كل طبقة بمراجع الدوال الأصلية.
def auth_required(func):
def wrapper(*args, **kwargs):
if not kwargs.get('user').is_authenticated:
raise PermissionError("Unauthorized access")
return func(*args, **kwargs)
return wrapper
def cache_result(func):
cache = {}
def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@auth_required
@cache_result
@log_execution_time
def get_user_data(user_id, user):
# محاكاة جلب بيانات من قاعدة بيانات
time.sleep(0.1)
return {"id": user_id, "name": "Test User"}
# عند الاستدعاء: get_user_data(1, user=current_user)
# الترتيب الفعلي للتنفيذ:
# 1. log_execution_time
# 2. cache_result
# 3. auth_required
# 4. الدالة الأصلية get_user_dataهنا تكمن المشكلة الخفية: كل زخرفة تضيف طبقة جديدة من الـ Closure، وكل طبقة تحتفظ بمراجع للكائنات التي تستخدمها. في المثال السابق، الـ cache في زخرفة cache_result يُنشئ قاموساً جديداً لكل دالة مزخرفة، مما قد يؤدي إلى تسرب ذاكرة إذا كانت الدالة تُستدعى بمجموعات مختلفة من الوسائط. الحل؟ استخدم كائنات خارجية للـ Caching مثل Redis أو حتى متغير مستوى الوحدة (Module-Level Variable) بدلاً من إنشاء قاموس جديد لكل دالة. أيضاً، لاحظ كيف أن زخرفة auth_required تعتمد على وسيط user الذي يُمرر عبر kwargs - هذا تصميم خطير لأنه يجعل الدالة تعتمد على وسيط غير ظاهر في توقيعها الأصلي، مما يصعب فهم الكود وصيانته.
أحد الأخطاء الشائعة التي يقع فيها حتى المطورون ذوو الخبرة هو استخدام وسيطات افتراضية قابلة للتغيير (Mutable Default Arguments) داخل الـ Decorators. المشكلة أن هذه الوسائط تُنشئ مرة واحدة فقط عند تعريف الدالة، وليس عند كل استدعاء، مما يؤدي إلى سلوك غير متوقع ومشاركتها بين جميع الاستدعاءات. لنأخذ مثالاً واقعياً من مشروع حقيقي:
def bad_logger(func):
log_messages = [] # هذا سيُنشئ مرة واحدة فقط!
def wrapper(*args, **kwargs):
log_messages.append(f"Calling {func.__name__} with {args}")
result = func(*args, **kwargs)
log_messages.append(f"Finished {func.__name__}")
print("\n".join(log_messages))
return result
return wrapper
@bad_logger
def add(a, b):
return a + b
# عند الاستدعاء:
# add(1, 2) # يطبع سجلين
# add(3, 4) # يطبع أربعة سجلات! (السجلين السابقين + الجديدين)الحل الصحيح هو استخدام None كقيمة افتراضية، ثم إنشاء الكائن داخل الـ Wrapper عند كل استدعاء:
def good_logger(func):
def wrapper(*args, **kwargs):
log_messages = [] # يُنشئ عند كل استدعاء
log_messages.append(f"Calling {func.__name__} with {args}")
result = func(*args, **kwargs)
log_messages.append(f"Finished {func.__name__}")
print("\n".join(log_messages))
return result
return wrapperالـ Decorators لا تقتصر على الدوال فقط، بل يمكن استخدامها مع الكلاسات أيضاً، وهذا يفتح الباب أمام إمكانيات مذهلة في تعديل سلوك الكلاسات نفسها. أحد الاستخدامات القوية هو تنفيذ نمط الـ Singleton دون تعديل الكود الأصلي للكلاس. لكن هنا تكمن مشكلة التصميم: الكلاسات المزخرفة تفقد بعض خصائصها الأصلية مثل الـ __name__ و __doc__، مما قد يؤثر على الـ Introspection وأدوات التوثيق.
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self, url):
self.url = url
print(f"Connecting to {url}")
# عند الاستدعاء:
# db1 = DatabaseConnection("postgres://localhost")
# db2 = DatabaseConnection("mysql://localhost")
# print(db1 is db2) # True! نفس الكائن يُعاد
# لكن لاحظ أن db1.url != db2.url - هذا خطأ في التصميم!المشكلة في هذا المثال هي أن الـ Singleton تحتفظ بمرجع للكلاس وليس للكائن نفسه بناءً على الوسائط. هذا يعني أنك إذا استخدمت وسائط مختلفة، ستحصل على نفس الكائن لكن مع حالة غير متوقعة. الحل هو استخدام مفتاح فريد يعتمد على الـ args و kwargs، أو الأفضل، استخدام نمط الـ Borg الذي يشارك الحالة بين جميع الكائنات بدلاً من فرض كائن واحد فقط. أيضاً، لاحظ كيف أن الكلاس المزخرف فقد اسمه الأصلي:
print(DatabaseConnection.__name__) # يُرجع 'get_instance' بدلاً من 'DatabaseConnection'لحل هذه المشكلة، استخدم وحدة functools التي توفر دالة wraps للحفاظ على خصائص الدالة الأصلية:
from functools import wraps
def singleton(cls):
instances = {}
@wraps(cls) # هذه السطر يحافظ على خصائص الكلاس الأصلية
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instanceعندما تتجاوز الاستخدامات البسيطة، تكتشف أن الـ Decorators يمكنها فعل أكثر بكثير من مجرد تسجيل الوقت أو فحص الصلاحيات. أحد الاستخدامات المتقدمة هو تنفيذ الـ Retry Logic للدوال التي تتعامل مع خدمات خارجية غير موثوقة. بدلاً من تكرار الكود في كل مكان، يمكنك كتابة زخرفة واحدة تتعامل مع إعادة المحاولة، الوقت الفاصل بين المحاولات، وحتى الـ Exponential Backoff. هذا ليس مجرد توفير وقت، بل هو تطبيق لمبدأ الـ DRY (Don't Repeat Yourself) على مستوى البنية التحتية للكود.
import time
import random
from functools import wraps
def retry(max_attempts=3, delay=1, backoff=2, excepti(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
current_delay = delay
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts == max_attempts:
raise
print(f"Attempt {attempts} failed. Retrying in {current_delay} seconds...")
time.sleep(current_delay)
current_delay *= backoff
return wrapper
return decorator
@retry(max_attempts=5, delay=0.1, backoff=2)
def unreliable_api_call():
if random.random() < 0.7: # 70% فرصة للفشل
raise ConnectionError("Service unavailable")
return "Success!"هذا المثال يظهر قوة الـ Decorators في التعامل مع منطق البنية التحتية. بدلاً من كتابة حلقة إعادة المحاولة لكل دالة تتعامل مع API، يمكنك استخدام زخرفة واحدة تغطي جميع الحالات. لكن لاحظ كيف أن الـ Decorator هنا يقبل وسائط - هذا نمط متقدم يسمى "Decorator Factory" حيث الدالة الخارجية تُرجع الـ Decorator الفعلي. هذا يسمح لك بتخصيص سلوك الزخرفة لكل استخدام دون تكرار الكود.
أحد الاستخدامات المتقدمة التي لا يتحدث عنها الكثيرون هو دمج الـ Decorators مع الـ Context Managers لتنفيذ منطق قبل وبعد الدالة بطريقة أكثر قوة ومرونة. بدلاً من كتابة with block داخل الدالة، يمكنك استخدام زخرفة تتعامل مع الـ Context تلقائياً. هذا مفيد بشكل خاص عند التعامل مع موارد مثل الملفات، اتصالات الشبكة، أو حتى الـ Thread Locks.
from contextlib import contextmanager
import threading
def with_lock(lock):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return wrapper
return decorator
# الاستخدام:
lock = threading.Lock()
@with_lock(lock)
def critical_section(data):
# هذا الكود يُنفذ داخل سياق الـ Lock
print(f"Processing {data}")
return data.upper()هذا النمط مفيد بشكل خاص في التطبيقات متعددة الخيوط حيث تحتاج إلى ضمان تنفيذ أقسام معينة من الكود بشكل متسلسل. بدلاً من تذكر كتابة with lock في كل مكان، الزخرفة تتعامل مع ذلك تلقائياً. لكن كن حذراً: استخدام الـ Locks بهذه الطريقة قد يؤدي إلى تعقيد تدفق التحكم ويجعل من الصعب تتبع حالات الـ Deadlock. دائماً وثق بوضوح متى وكيف تُستخدم الزخارف التي تضيف سلوكاً متزامناً (Synchronous Behavior).
بعد أكثر من عشر سنوات في كتابة بايثون بشكل احترافي، هذه هي نصيحتي الصريحة لك: استخدم الـ Decorators عندما تريد إضافة سلوك عابر (Cross-Cutting Concern) لا يتعلق بالمنطق الأساسي للدالة، مثل الـ Logging، الـ Caching، أو فحص الصلاحيات. تجنبها عندما يكون السلوك جزءاً لا يتجزأ من منطق الدالة نفسها، أو عندما يؤدي استخدامها إلى تعقيد الكود دون قيمة واضحة. أيضاً، تذكر دائماً أن الـ Decorators تضيف طبقة من الغموض للكود - المطور التالي الذي يقرأ الكود قد لا يفهم بسهولة ما يحدث خلف الكواليس، لذا وثق استخدامها جيداً وكن حذراً من الـ Side Effects التي قد تسببها.
القاعدة الذهبية التي أتبعها: إذا وجدت نفسك تكتب نفس الكود ثلاث مرات أو أكثر حول دوال مختلفة، فكر في استخدام زخرفة. لكن إذا كانت الزخرفة تجعل الكود أصعب في الفهم من الكود الأصلي بدونها، فتخلص منها واكتب الكود بشكل صريح. أيضاً، دائماً استخدم functools.wraps للحفاظ على خصائص الدوال الأصلية، واختبر أداء الكود المزخرف بعناية - أحياناً يكون الـ Overhead الذي تضيفه الزخرفة غير مقبول في الأنظمة عالية الأداء. وأخيراً، تذكر أن بايثون تمنحك القوة لتعديل سلوك الدوال في وقت التشغيل، لكن مع هذه القوة تأتي مسؤولية كبيرة: استخدمها بحكمة.