الديكوريترز في بايثون ليست مجرد سكر نحوي، بل أداة قوية تُغير طريقة كتابة الكود. اكتشف كيف تحول من فهم سطحي إلى استخدام احترافي يوفر ساعات من الديباج ويجعل الكود أنظف وأكثر مرونة.
تخيل أنك تعمل على مشروع ضخم يحتوي على مئات الدوال، وكلها تحتاج إلى تسجيل وقت التنفيذ، أو التحقق من الصلاحيات، أو تسجيل الأخطاء. بدون ديكوريترز، ستجد نفسك تكرر نفس الكود في كل دالة، مما يجعل الصيانة كابوساً. لكن مع ديكوريترز، يمكنك كتابة منطق واحد وتطبيقه على أي دالة تريدها بنقرة واحدة. هذا ليس سحراً، بل هو ما تفعله الديكوريترز خلف الكواليس: إنها دوال تأخذ دوالاً أخرى وتعيد دوالاً معدلة، مع الاحتفاظ بالسلوك الأصلي.
في البداية، قد تبدو الديكوريترز وكأنها مفهوم مجرد، خاصة عندما ترى الرموز الغريبة مثل @decorator فوق الدوال. لكن الحقيقة هي أن الديكوريترز ليست معقدة كما تبدو. إنها ببساطة دوال تُستخدم لتعديل سلوك دوال أخرى دون تغيير الكود الأصلي. وهذا ما يجعلها أداة قوية في يد المطورين الذين يريدون كتابة كود نظيف وقابل لإعادة الاستخدام.
عندما تكتب @log_execution_time فوق دالة، فإن بايثون تقوم بتحويل هذا الكود إلى شيء مثل: original_function = log_execution_time(original_function). هذا يعني أن الدالة الأصلية تُمرر كوسيط إلى الديكوريتر، الذي يعيد دالة جديدة تحيط بالدالة الأصلية. وعندما تُستدعى الدالة الأصلية، فإن الدالة الجديدة هي التي تُنفذ أولاً، مما يسمح لك بإضافة سلوك إضافي قبل أو بعد تنفيذ الدالة الأصلية.
لفهم هذا بشكل أعمق، دعنا ننظر إلى الذاكرة. عندما تُعرف دالة في بايثون، فإنها تُخزن ككائن في الذاكرة، ولها عنوان محدد. الديكوريتر يأخذ هذا الكائن، ويخلق كائن دالة جديد، ويحتفظ بالمرجع إلى الدالة الأصلية داخل الكائن الجديد. هذا يعني أن الدالة الجديدة لديها وصول إلى الدالة الأصلية ويمكنها استدعاؤها متى شاءت. وهذا هو السبب في أن الديكوريترز يمكنها إضافة سلوك قبل أو بعد استدعاء الدالة الأصلية، أو حتى استبدالها بالكامل.
def log_execution_time(func):
def wrapper(*args, **kwargs):
import time
start_time = time.time()
result = func(*args, **kwargs) # استدعاء الدالة الأصلية
end_time = time.time()
print(f"Execution time of {func.__name__}: {end_time - start_time:.4f} seconds")
return result
return wrapper
@log_execution_time
def calculate_factorial(n):
if n == 0:
return 1
else:
return n * calculate_factorial(n - 1)
print(calculate_factorial(10))في المثال أعلاه، الدالة calculate_factorial تُزين بديكوريتر log_execution_time. عندما تُستدعى calculate_factorial(10)، فإن الدالة wrapper داخل الديكوريتر هي التي تُنفذ أولاً. تقوم wrapper بحساب وقت البداية، ثم تستدعي الدالة الأصلية calculate_factorial، ثم تحسب وقت النهاية وتطبع الفرق. النتيجة النهائية هي أن الدالة الأصلية تُنفذ كما هي، ولكن مع إضافة سلوك تسجيل وقت التنفيذ.
الديكوريترز البسيطة رائعة، لكن ماذا لو أردت تمرير وسائط إلى الديكوريتر نفسه؟ مثلاً، تريد تسجيل وقت التنفيذ فقط إذا كان الوقت المتوقع أطول من قيمة معينة. هنا تأتي الديكوريترز مع وسائط، وهي أكثر تعقيداً قليلاً ولكنها أقوى بكثير. الفكرة هي أن الديكوريتر مع وسائط هو في الواقع دالة تُرجع ديكوريتراً آخر، والذي بدوره يُرجع الدالة المعدلة.
لفهم هذا، تخيل أنك تريد إنشاء ديكوريتر يُسمح لك بتحديد مستوى تسجيل الأخطاء (مثل DEBUG، INFO، ERROR). بدلاً من كتابة ديكوريتر مختلف لكل مستوى، يمكنك كتابة ديكوريتر واحد يقبل مستوى التسجيل كوسيط. هذا يتطلب طبقة إضافية من الدوال: دالة خارجية تقبل الوسائط، ودالة داخلية تُرجع الديكوريتر الفعلي، والدالة المعدلة التي تُنفذ السلوك الفعلي.
def log_level(level):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{level}] Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"[{level}] Finished calling: {func.__name__}")
return result
return wrapper
return decorator
@log_level("DEBUG")
def add(a, b):
return a + b
@log_level("INFO")
def multiply(a, b):
return a * b
print(add(3, 5))
print(multiply(4, 6))في هذا المثال، الدالة log_level هي دالة تُرجع الديكوريتر decorator. وعندما تُستخدم @log_level("DEBUG")، فإنها تُمرر الوسيط "DEBUG" إلى log_level، والتي تُرجع الديكوريتر decorator. هذا الديكوريتر بدوره يُزين الدالة add، مضيفاً سلوك تسجيل الدخول مع المستوى المحدد. النتيجة هي أن كل استدعاء للدالة add سيطبع رسائل تسجيل الدخول مع المستوى "DEBUG".
الديكوريترز ليست مجرد أداة لتسجيل الدخول أو قياس الأداء. في المشاريع الحقيقية، تُستخدم الديكوريترز لحل مشاكل معقدة مثل التحقق من الصلاحيات، التخزين المؤقت (Caching)، وإدارة الموارد. على سبيل المثال، في إطار عمل Flask، تُستخدم الديكوريترز لتحديد مسارات الويب. وعندما تُكتب @app.route("/home") فوق دالة، فإن Flask يستخدم الديكوريتر لتسجيل الدالة كمسار للويب، مما يسمح للسيرفر بمعرفة أي دالة يجب استدعاؤها عند زيارة المسار "/home".
في تجربتي، استخدمت الديكوريترز لحل مشكلة شائعة في الأنظمة الموزعة: التحقق من الصلاحيات. بدلاً من كتابة كود التحقق داخل كل دالة، كتبت ديكوريتراً واحداً يتحقق من صلاحيات المستخدم قبل السماح بتنفيذ الدالة. هذا لم يجعل الكود أنظف فحسب، بل جعله أيضاً أكثر أماناً، لأن منطق التحقق موجود في مكان واحد ويمكن تعديله بسهولة دون الحاجة إلى تغيير كل دالة على حدة.
def require_permission(permission):
def decorator(func):
def wrapper(user, *args, **kwargs):
if permission not in user.permissions:
raise PermissionError(f"User does not have {permission} permission")
return func(user, *args, **kwargs)
return wrapper
return decorator
class User:
def __init__(self, permissions):
self.permissi permissions
@require_permission("admin")
def delete_user(user, user_id):
print(f"Deleting user with ID: {user_id}")
admin_user = User(["admin", "edit"])
regular_user = User(["edit"])
delete_user(admin_user, 123) # يعمل بدون مشاكل
# delete_user(regular_user, 123) # يرفع PermissionErrorفي هذا المثال، الديكوريتر require_permission يتحقق من أن المستخدم لديه الصلاحية المطلوبة قبل السماح بتنفيذ الدالة. إذا لم يكن لدى المستخدم الصلاحية، فإن الديكوريتر يرفع خطأ PermissionError. هذا يجعل الكود أكثر أماناً ومرونة، حيث يمكن إضافة أو إزالة الصلاحيات بسهولة دون الحاجة إلى تعديل كل دالة على حدة.
إحدى الاستخدامات الأكثر قوة للديكوريترز هي التخزين المؤقت. تخيل أنك تعمل على دالة تستغرق وقتاً طويلاً لحساب نتيجة معينة، مثل دالة تحسب الأعداد الأولية الكبيرة. إذا كانت هذه الدالة تُستدعى بشكل متكرر بنفس الوسائط، فإن إعادة حساب النتيجة في كل مرة يعد إهداراً للموارد. بدلاً من ذلك، يمكنك استخدام ديكوريتر لتخزين النتائج مؤقتاً، بحيث إذا تم استدعاء الدالة بنفس الوسائط مرة أخرى، فإنها تُرجع النتيجة المخزنة بدلاً من إعادة الحساب.
هذا النوع من التخزين المؤقت يُعرف باسم Memoization، وهو تقنية شائعة لتحسين أداء الدوال البطيئة. باستخدام الديكوريترز، يمكنك تطبيق Memoization بسهولة على أي دالة تريدها، دون الحاجة إلى تعديل الكود الأصلي للدالة. هذا يجعل الكود أكثر قابلية للصيانة، حيث يمكن إضافة أو إزالة التخزين المؤقت ببساطة عن طريق إضافة أو إزالة الديكوريتر.
def memoize(func):
cache = {}
def wrapper(*args):
if args in cache:
print(f"Returning cached result for {args}")
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(10)) # الحساب الأول يستغرق وقتاً
print(fibonacci(10)) # النتيجة تُرجع من الكاش مباشرةفي هذا المثال، الدالة fibonacci تُزين بديكوريتر memoize. عندما تُستدعى fibonacci(10) لأول مرة، فإنها تحسب النتيجة وتخزنها في الكاش. وعندما تُستدعى مرة أخرى بنفس الوسائط، فإنها تُرجع النتيجة المخزنة بدلاً من إعادة الحساب. هذا يُحسن الأداء بشكل كبير، خاصة للدوال العودية مثل fibonacci التي تُستدعى بنفس الوسائط مراراً وتكراراً.
رغم أن الديكوريترز أداة قوية، إلا أنها قد تُسبب مشاكل إذا لم تُستخدم بحذر. إحدى المشاكل الشائعة هي فقدان البيانات الوصفية للدوال. عندما تُزين دالة بديكوريتر، فإن الدالة الناتجة قد تفقد المعلومات الأصلية مثل اسم الدالة (__name__)، ووثائقها (__doc__)، وأنواع وسائطها. هذا قد يُسبب مشاكل في أدوات التوثيق أو الاختبار التي تعتمد على هذه البيانات.
على سبيل المثال، إذا استخدمت دالة مثل help() على دالة مزينة، فقد ترى معلومات الدالة المعدلة بدلاً من الدالة الأصلية. لحل هذه المشكلة، يمكنك استخدام وحدة functools في بايثون، وتحديداً الدالة wraps. هذه الدالة تُنسخ البيانات الوصفية من الدالة الأصلية إلى الدالة المعدلة، مما يحافظ على المعلومات الأصلية.
from functools import wraps
def log_execution_time(func):
@wraps(func) # يحافظ على البيانات الوصفية للدالة الأصلية
def wrapper(*args, **kwargs):
import time
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Execution time of {func.__name__}: {end_time - start_time:.4f} seconds")
return result
return wrapper
@log_execution_time
def calculate_sum(n):
"""Calculate the sum of the first n natural numbers."""
return n * (n + 1) // 2
print(calculate_sum.__name__) # يطبع: calculate_sum
print(calculate_sum.__doc__) # يطبع: Calculate the sum of the first n natural numbers.في هذا المثال، استخدام @wraps(func) داخل الديكوريتر يحافظ على البيانات الوصفية للدالة الأصلية calculate_sum. بدون هذا السطر، كانت الدالة المعدلة ستفقد اسمها ووثائقها، مما قد يُسبب مشاكل في أدوات التوثيق أو الاختبار.
مشكلة أخرى شائعة هي الديكوريترز التي تُسبب تسرب الذاكرة. إذا كان الديكوريتر يحتفظ بمراجع إلى الكائنات التي تُمرر إليه، فقد يمنع هذه الكائنات من التحرر من الذاكرة. على سبيل المثال، إذا كان الديكوريتر يحتفظ بقائمة من النتائج في الكاش دون حد أقصى، فقد يؤدي ذلك إلى استهلاك الذاكرة بشكل غير محدود. لحل هذه المشكلة، يجب تصميم الديكوريترز بعناية للتأكد من أنها لا تحتفظ بمراجع غير ضرورية.
في بعض الأحيان، قد تحتاج إلى استخدام أكثر من ديكوريتر على نفس الدالة. على سبيل المثال، قد تريد تسجيل وقت التنفيذ والتحقق من الصلاحيات في نفس الوقت. في هذه الحالة، يمكنك استخدام ديكوريترز متداخلة، حيث يُطبق كل ديكوريتر على النتيجة التي يُرجعها الديكوريتر السابق. لكن هنا تأتي المشكلة: ترتيب الديكوريترز مهم جداً، وقد يؤدي تغيير الترتيب إلى سلوك مختلف تماماً.
لفهم هذا، تخيل أنك لديك ديكوريترين: أحدهما يتحقق من الصلاحيات، والآخر يسجل وقت التنفيذ. إذا وضعت ديكوريتر تسجيل وقت التنفيذ أولاً، ثم ديكوريتر التحقق من الصلاحيات، فإن وقت التنفيذ سيشمل وقت التحقق من الصلاحيات أيضاً. أما إذا وضعت ديكوريتر التحقق من الصلاحيات أولاً، فإن وقت التنفيذ لن يشمل وقت التحقق من الصلاحيات. هذا قد يكون مهماً إذا كنت تريد قياس أداء الدالة نفسها دون تأثير الديكوريترز الأخرى.
def log_execution_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Execution time of {func.__name__}: {end_time - start_time:.4f} seconds")
return result
return wrapper
def require_permission(permission):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if permission not in user.permissions:
raise PermissionError(f"User does not have {permission} permission")
return func(user, *args, **kwargs)
return wrapper
return decorator
class User:
def __init__(self, permissions):
self.permissi permissions
# الترتيب مهم هنا!
@log_execution_time
@require_permission("admin")
def delete_user(user, user_id):
print(f"Deleting user with ID: {user_id}")
admin_user = User(["admin", "edit"])
delete_user(admin_user, 123)في هذا المثال، الدالة delete_user مزينة بديكوريترين: require_permission و log_execution_time. الترتيب هنا مهم جداً. أولاً، يُطبق ديكوريتر require_permission، والذي يتحقق من الصلاحيات. ثم، يُطبق ديكوريتر log_execution_time، والذي يسجل وقت التنفيذ. إذا قمت بعكس الترتيب، فإن وقت التنفيذ سيشمل وقت التحقق من الصلاحيات أيضاً، مما قد لا يكون ما تريده إذا كنت تريد قياس أداء الدالة نفسها فقط.
الديكوريترز في بايثون هي واحدة من تلك الأدوات التي تبدو بسيطة في البداية، لكنها تُغير طريقة تفكيرك في البرمجة عندما تتعمق فيها. إنها ليست مجرد سكر نحوي لجعل الكود يبدو أجمل، بل هي أداة قوية تُمكنك من فصل الاهتمامات (Separation of Concerns) وجعل الكود أكثر قابلية لإعادة الاستخدام والصيانة. عندما تتقن الديكوريترز، ستجد نفسك تستخدمها في كل مكان، من تسجيل الدخول إلى التحقق من الصلاحيات، ومن التخزين المؤقت إلى إدارة الموارد.
لكن تذكر: مع القوة تأتي المسؤولية. استخدم الديكوريترز بحكمة، واحرص على الحفاظ على البيانات الوصفية للدوال، وتجنب تسرب الذاكرة، وكن حذراً مع الديكوريترز المتداخلة. إذا فعلت ذلك، ستجد أن الديكوريترز تصبح واحدة من أقوى الأدوات في ترسانتك البرمجية، وتساعدك على كتابة كود أنظف وأكثر مرونة.
نصيحة أخيرة: ابدأ باستخدام الديكوريترز البسيطة مثل تسجيل الدخول أو قياس الأداء، ثم انتقل تدريجياً إلى الحالات الأكثر تعقيداً مثل التخزين المؤقت والتحقق من الصلاحيات. كلما استخدمت الديكوريترز أكثر، كلما أصبحت أكثر راحة معها، وستبدأ في رؤية الفرص لاستخدامها في أماكن لم تفكر فيها من قبل. الديكوريترز ليست مجرد أداة، بل هي طريقة تفكير جديدة في البرمجة.