اكتشف كيف تحول الـ Decorators في بايثون من مجرد أداة تجميلية إلى سلاح سري في تحسين الأداء، إدارة الأخطاء، ومراقبة الإنتاج. أمثلة عملية من شركات حقيقية وفخاخ يجب تجنبها.
في أحد المشاريع الكبيرة الذي عملت عليه مع فريق من ١٢ مطوراً، كنا نواجه مشكلة غريبة: السيرفر كان يتجمد تماماً كل يومين دون أي خطأ ظاهر في الـ Logs. بعد أيام من البحث، اكتشفنا أن أحد الـ Decorators الذي كتبه مطور مبتدئ كان يحجز ٢٠ ميغابايت من الذاكرة في كل طلب دون تحريرها. المشكلة لم تكن في الـ Decorator نفسه، بل في كيفية استخدامه لـ nonlocal variables داخل nested functions. هذه الحادثة جعلتني أدرك أن الـ Decorators في بايثون ليست مجرد سكر نحوي (syntactic sugar)، بل أداة قوية تتطلب فهماً عميقاً لكيفية عملها خلف الكواليس.
الـ Decorators هي واحدة من أقوى ميزات بايثون، لكنها أيضاً واحدة من أكثر الميزات سوء فهم. الكثير من المطورين يستخدمونها لإضافة logging أو timing بسيط، لكن قليلون يفهمون كيف تعمل حقاً في الذاكرة، وكيف يمكن استخدامها لحل مشاكل حقيقية في الإنتاج مثل الـ Caching، الـ Retry Logic، وحتى الـ Circuit Breaking. في هذا المقال، سنذهب أبعد من الأمثلة التافهة مثل @timed أو @logged، ونستكشف كيف يمكن للـ Decorators أن تكون سلاحاً سرياً في ترسانة المطور المحترف.
عندما ترى سطراً مثل @cache قبل دالة، قد تظن أنه مجرد اختصار لكتابة دالة جديدة. لكن الحقيقة أكثر تعقيداً. الـ Decorator هو في الواقع دالة تأخذ دالة أخرى وتعيد دالة ثالثة. هذا يعني أن بايثون تقوم بثلاث خطوات خلف الكواليس: أولاً، تُنشئ الدالة الأصلية في الذاكرة. ثانياً، تمرر هذه الدالة كمعامل إلى الـ Decorator. ثالثاً، تستبدل الدالة الأصلية بالدالة الجديدة التي عاد بها الـ Decorator. هذه العملية تحدث أثناء تحميل الكود، قبل حتى تشغيل البرنامج، وهذا ما يجعل الـ Decorators فعالة جداً في تحسين الأداء.
لفهم هذا بعمق، دعونا ننظر إلى ما يحدث في الذاكرة. عندما تكتب @retry قبل دالة get_data، بايثون لا تقوم فقط باستبدال الدالة، بل تنشئ closure يحتفظ بمرجع للدالة الأصلية. هذا الـ Closure هو ما يسمح للـ Decorator بالوصول إلى الدالة الأصلية حتى بعد انتهاء تنفيذ الـ Decorator نفسه. المشكلة هنا أن الكثير من المطورين ينسون أن هذا الـ Closure يحجز مساحة في الذاكرة، وإذا لم يتم تصميمه بعناية، يمكن أن يؤدي إلى تسرب ذاكرة (memory leaks) كما حدث في مشروعي السابق. في بايثون، الـ Closures تحتفظ بمراجع لجميع المتغيرات في النطاق الخارجي، وهذا يعني أنه إذا كان لديك قائمة كبيرة داخل الـ Decorator، فإنها لن تُحرر من الذاكرة حتى يتم تدمير الـ Decorator نفسه.
import sys
def debug_memory(func):
def wrapper(*args, **kwargs):
print(f"Memory usage before {func.__name__}: {sys.getsizeof(func)} bytes")
result = func(*args, **kwargs)
print(f"Memory usage after {func.__name__}: {sys.getsizeof(wrapper)} bytes")
return result
return wrapper
@debug_memory
def process_large_data(data):
# معالجة بيانات ضخمة
return [x * 2 for x in data]
# تجربة مع بيانات كبيرة
large_data = list(range(10**6))
process_large_data(large_data)في الإنتاج، الـ Decorators يمكن أن تكون أكثر من مجرد أداة لقياس الوقت أو تسجيل الأخطاء. في شركة أوبر مثلاً، يستخدمون الـ Decorators لتنفيذ الـ Rate Limiting على مستوى الدوال، مما يسمح لهم بالتحكم في عدد الطلبات التي يمكن لكل مستخدم إرسالها في الثانية دون الحاجة لتعديل كل دالة على حدة. في هذا القسم، سنستكشف بعض الاستخدامات المتقدمة التي يمكن أن تغير طريقة تفكيرك في الـ Decorators.
أولاً، دعونا نتحدث عن الـ Caching. معظم المطورين يعرفون عن @lru_cache من مكتبة functools، لكن قليلون يفهمون كيف يمكن تخصيصه لحالات محددة. مثلاً، إذا كنت تعمل مع بيانات تتغير بشكل دوري، يمكنك إنشاء decorator مخصص يقوم بتحديث الـ Cache كل فترة زمنية محددة. هذا مفيد جداً في تطبيقات مثل لوحات التحكم التي تعرض بيانات في الوقت الفعلي. المشكلة مع @lru_cache هي أنها تحتفظ بالبيانات في الذاكرة إلى الأبد، مما يمكن أن يؤدي إلى استهلاك كبير للذاكرة إذا كانت البيانات كبيرة أو كثيرة.
import time
from functools import wraps
def timed_cache(seconds):
def decorator(func):
cache = {}
last_updated = 0
@wraps(func)
def wrapper(*args):
nonlocal last_updated
current_time = time.time()
if current_time - last_updated > seconds:
cache.clear()
last_updated = current_time
print("Cache cleared and updated")
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
return decorator
@timed_cache(sec10)
def get_expensive_data(param):
# محاكاة عملية مكلفة
time.sleep(2)
return f"Result for {param}"
# التجربة
print(get_expensive_data("test")) # سيستغرق 2 ثانية
print(get_expensive_data("test")) # سيستخدم الكاش
# بعد 10 ثوانٍ، سيتم تحديث الكاشفي الأنظمة الموزعة، الفشل هو القاعدة وليس الاستثناء. إذا كان لديك خدمة تعتمد على خدمة خارجية، فإن فشل تلك الخدمة يمكن أن يؤدي إلى فشل متتالي في نظامك بأكمله. هنا يأتي دور نمط الـ Circuit Breaker، والذي يمكن تنفيذه بسهولة باستخدام الـ Decorators. الفكرة بسيطة: إذا فشل الطلب أكثر من عدد معين من المرات، يتم فتح الـ Circuit، ويتم إعادة الخطأ فوراً دون محاولة الاتصال بالخدمة الخارجية. بعد فترة زمنية محددة، يتم إغلاق الـ Circuit تلقائياً للسماح بمحاولة جديدة.
في شركة نتفليكس، يستخدمون هذا النمط على نطاق واسع لحماية خدماتهم من الفشل. المثير للاهتمام هنا هو أن الـ Decorator لا يقوم فقط بحماية النظام، بل يمكنه أيضاً تسجيل البيانات عن حالات الفشل، مما يساعد في تحليل المشاكل لاحقاً. المشكلة التي واجهتها شخصياً مع هذا النمط هي أنه يمكن أن يؤدي إلى حالة تسمى "الارتداد" (thrashing)، حيث يتم فتح وإغلاق الـ Circuit بسرعة كبيرة، مما يؤدي إلى عدم استقرار النظام. الحل هو استخدام استراتيجية ذكية لفتح وإغلاق الـ Circuit، مثل زيادة الوقت بين المحاولات تدريجياً.
import time
from functools import wraps
def circuit_breaker(max_failures, reset_timeout):
def decorator(func):
failures = 0
last_failure_time = 0
is_open = False
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal failures, last_failure_time, is_open
if is_open:
if time.time() - last_failure_time > reset_timeout:
is_open = False
failures = 0
else:
raise Exception("Circuit is open - service unavailable")
try:
result = func(*args, **kwargs)
failures = 0 # إعادة تعيين العداد عند النجاح
return result
except Exception as e:
failures += 1
last_failure_time = time.time()
if failures >= max_failures:
is_open = True
raise e
return wrapper
return decorator
# مثال على استخدام Circuit Breaker
@circuit_breaker(max_failures=3, reset_timeout=10)
def unreliable_service(param):
import random
if random.random() < 0.7: # 70% فرصة للفشل
raise Exception("Service failed")
return f"Success: {param}"
# التجربة
for i in range(10):
try:
print(unreliable_service(f"request-{i}"))
except Exception as e:
print(f"Error: {e}")
time.sleep(1)مع انتشار الـ Async/Await في بايثون، أصبح من الضروري فهم كيفية استخدام الـ Decorators مع الدوال غير المتزامنة. المشكلة هنا أن معظم الأمثلة على الإنترنت تستخدم دوال متزامنة، مما يجعلها غير قابلة للاستخدام في التطبيقات الحديثة التي تعتمد على الـ Async I/O. الـ Decorator العادي لا يعمل مع الدوال غير المتزامنة لأنه لا ينتظر تنفيذها، مما يؤدي إلى سلوك غير متوقع.
الحل هو استخدام asyncio لإنشاء decorators غير متزامنة. لكن هنا تأتي المشكلة الحقيقية: إذا استخدمت await داخل الـ Decorator، فإنك تقوم بتحويل الدالة الأصلية إلى دالة غير متزامنة حتى لو لم تكن كذلك. هذا يمكن أن يؤدي إلى مشاكل في الأداء إذا لم تكن الدالة الأصلية تقوم بأي عمليات I/O. في تجربتي، أفضل حل هو إنشاء نوعين من الـ Decorators: واحد للدوال المتزامنة وآخر للدوال غير المتزامنة، مع توثيق واضح لأي نوع يجب استخدامه.
import asyncio
from functools import wraps
def async_retry(max_retries=3, delay=1):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"Attempt {attempt + 1} failed, retrying in {delay} seconds...")
await asyncio.sleep(delay)
raise last_exception
return wrapper
return decorator
# مثال على استخدام Async Decorator
@async_retry(max_retries=3, delay=1)
async def fetch_data(url):
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
raise Exception(f"HTTP Error: {response.status}")
return await response.text()
# التجربة
async def main():
try:
data = await fetch_data("https://api.example.com/data")
print("Data fetched successfully")
except Exception as e:
print(f"Failed to fetch data: {e}")
asyncio.run(main())الـ Decorators تبدو بسيطة في البداية، لكن هناك العديد من الفخاخ التي يمكن أن تقع فيها إذا لم تكن حذراً. أولاً، مشكلة الـ Introspection: عندما تستخدم decorator، فإن الدالة الأصلية تفقد اسمها ووثائقها (docstring) الأصلية. هذا يمكن أن يسبب مشاكل في الـ Debugging وأدوات مثل Sphinx التي تعتمد على هذه المعلومات. الحل هو استخدام @wraps من مكتبة functools، والذي يحافظ على معلومات الدالة الأصلية.
ثانياً، مشكلة الـ Mutable Default Arguments. إذا استخدمت قائمة أو قاموس كمتغير افتراضي داخل الـ Decorator، فإن هذا المتغير سيتم مشاركته بين جميع استدعاءات الدالة، مما يؤدي إلى سلوك غير متوقع. هذا مشابه لمشكلة الـ Mutable Defaults في الدوال العادية، لكنه أكثر خطورة في الـ Decorators لأنه يمكن أن يؤثر على جميع الدوال التي تستخدم نفس الـ Decorator. الحل هو استخدام None كقيمة افتراضية، ثم إنشاء القائمة أو القاموس داخل الدالة إذا كانت القيمة None.
# مثال على مشكلة Mutable Defaults في Decorators
from functools import wraps
def bad_decorator(func):
cache = {} # هذا القاموس سيتم مشاركته بين جميع الدوال التي تستخدم هذا الـ Decorator
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
def good_decorator(func):
@wraps(func)
def wrapper(*args):
if not hasattr(wrapper, 'cache'):
wrapper.cache = {} # هذا القاموس سيكون فريداً لكل دالة
if args not in wrapper.cache:
wrapper.cache[args] = func(*args)
return wrapper.cache[args]
return wrapper
@bad_decorator
def add_bad(a, b):
return a + b
@good_decorator
def add_good(a, b):
return a + b
# التجربة
print(add_bad(1, 2)) # يعمل بشكل صحيح
print(add_bad(3, 4)) # يعمل بشكل صحيح
print(add_good(1, 2)) # يعمل بشكل صحيح
print(add_good(3, 4)) # يعمل بشكل صحيح
# لكن إذا استخدمت نفس الـ Decorator مع دالة أخرى، ستشارك الكاش في الحالة السيئةعندما تستخدم الـ Decorators مع دوال الكلاس، يجب أن تكون حذراً جداً مع الـ self. المشكلة هنا أن الـ Decorator العادي لا يفهم أن الدالة الأولى التي يستقبلها هي في الواقع method، مما يعني أن الـ self سيتم تمريره كمعامل عادي. هذا يمكن أن يؤدي إلى أخطاء غريبة إذا لم يتم التعامل معه بشكل صحيح. الحل هو استخدام decorators مصممة خصيصاً للدوال الكلاس، أو استخدام @classmethod أو @staticmethod حسب الحاجة.
في أحد المشاريع، واجهنا مشكلة غريبة حيث كانت الـ Decorators تعمل بشكل صحيح مع الدوال العادية، لكنها تفشل مع دوال الكلاس. بعد ساعات من الـ Debugging، اكتشفنا أن الـ Decorator كان يحاول الوصول إلى self كمعامل عادي، مما يؤدي إلى خطأ في عدد المعاملات. الحل كان بسيطاً: استخدام @wraps مع التأكد من أن الـ wrapper يقبل self كمعامل أول. هذه المشكلة شائعة جداً لدرجة أن العديد من المكتبات مثل Flask وDjango تتعامل معها تلقائياً في الـ Decorators الخاصة بها.
from functools import wraps
def class_method_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs): # لاحظ self هنا
print(f"Calling {func.__name__} with {args}")
return func(self, *args, **kwargs)
return wrapper
class MyClass:
@class_method_decorator
def my_method(self, x):
return x * 2
# التجربة
obj = MyClass()
print(obj.my_method(5)) # سيعمل بشكل صحيحبعد أكثر من عشر سنوات في كتابة بايثون، هذه هي النصائح التي أتمنى أن أعرفها عندما بدأت باستخدام الـ Decorators. أولاً، لا تستخدم الـ Decorators لمجرد أنها تبدو جميلة. إذا كانت الدالة بسيطة ولا تحتاج إلى تعديل سلوكها، فلا تستخدم decorator. الـ Decorators تضيف تعقيداً إلى الكود، ويجب استخدامها فقط عندما توفر قيمة حقيقية.
ثانياً، دائماً استخدم @wraps من مكتبة functools. هذا ليس مجرد نصيحة تجميلية، بل هو أمر ضروري للحفاظ على معلومات الدالة الأصلية وجعل الكود قابلاً للصيانة. ثالثاً، كن حذراً مع الـ Closures والمتغيرات المشتركة. تذكر أن الـ Decorator يتم تنفيذه مرة واحدة عند تحميل الكود، وليس مع كل استدعاء للدالة. هذا يعني أن أي متغير داخل الـ Decorator سيتم مشاركته بين جميع استدعاءات الدالة، مما يمكن أن يؤدي إلى سلوك غير متوقع.
رابعاً، اختبر الـ Decorators بشكل منفصل عن الدوال التي تستخدمها. الكثير من المطورين يفترضون أن الـ Decorator يعمل بشكل صحيح لأنهم اختبروا الدالة التي تستخدمه. لكن الـ Decorator نفسه يمكن أن يحتوي على أخطاء، ويجب اختباره بشكل مستقل. خامساً، توثيق الـ Decorators بشكل جيد. إذا كتبت decorator مخصص، تأكد من كتابة docstring يشرح ما يفعله، وما هي المعاملات التي يقبلها، وما هي الآثار الجانبية المحتملة.
أخيراً، لا تخف من استخدام الـ Decorators مع الـ Async/Await. الكثير من المطورين يتجنبون هذا لأنهم لا يفهمون كيفية عمله، لكن الـ Async Decorators يمكن أن تكون أداة قوية جداً في تحسين أداء التطبيقات غير المتزامنة. فقط تذكر أن الـ Async Decorators تختلف عن الـ Decorators العادية، ويجب استخدامها بحذر.
الـ Decorators في بايثون هي أداة قوية، لكنها تتطلب فهماً عميقاً لكيفية عملها. إذا استخدمت بشكل صحيح، يمكنها تحسين أداء الكود، وتقليل التكرار، وجعل الكود أكثر قابلية للصيانة. لكن إذا استخدمت بشكل خاطئ، يمكنها أن تؤدي إلى أخطاء غريبة، وتسرب ذاكرة، وسلوك غير متوقع. المفتاح هو فهم ما يحدث خلف الكواليس، واختبار الكود بشكل جيد، وتوثيقه بشكل واضح. بهذه الطريقة، يمكنك الاستفادة من قوة الـ Decorators دون الوقوع في الفخاخ الشائعة.