كيف تحول ديكوريترز في بايثون من أداة تجميلية إلى حلول حقيقية تخفض وقت التنفيذ 40% وتقلل أكواد التكرار في مشاريع الإنتاج؟ اكتشف الأسرار التي لا يخبرك بها الدروس التقليدية.
في الأسبوع الماضي، علق سيرفر الإنتاج لشركة ناشئة في مجال الفنتك لمدة ١٢ ساعة لأن أحدهم قرر تسجيل كل طلب API في قاعدة بيانات منفصلة. النتيجة؟ ٣٠٠ ألف سجل غير ضروري يبطئ الاستعلامات ويستهلك ٢ جيجابايت من الذاكرة. المشكلة لم تكن في منطق العمل، بل في أن المطورين أضافوا كود الـ logging يدوياً في كل دالة، وتضارب تعريف الـ retry mechanism في ثلاث طبقات مختلفة. لو استخدموا ديكوريترز بشكل صحيح، لما حدث هذا الكابوس. الديكوريترز ليست مجرد سكر سينتاكسي، بل هي أداة هندسية حقيقية تحل مشاكل الإنتاج الحقيقية - إذا عرفت كيف تستخدمها.
المشكلة الأكبر أن معظم المطورين يتعلمون الديكوريترز كمفهوم مجرد: دالة تأخذ دالة وتعيد دالة. لكنهم لا يفهمون كيف تعمل خلف الكواليس، متى يجب استخدامها، ومتى تكون كارثة على الأداء. مثلاً، هل تعلم أن استخدام ديكوريتر مع @property داخل حلقة تكرارية كبيرة يمكن أن يزيد وقت التنفيذ بنسبة ٣٠٠٪ بسبب الـ function call overhead؟ في هذا المقال، سنفكك الديكوريترز من الصفر إلى الاحتراف، مع أمثلة حقيقية من مشاريع إنتاجية، وأخطاء شائعة تدمر الأداء، وحلول عملية تستخدمها فرق التطوير في شركات مثل أوبر وسبوتيفاي.
عندما تكتب @decorator فوق دالة، بايثون لا تفعل شيئاً سحرياً. إنها ببساطة تنفذ الكود التالي خلف الكواليس: original_function = decorator(original_function). هذا يعني أن الدالة الأصلية تُستبدل بالدالة التي يرجعها الديكوريتر. لكن ما لا يخبرك به معظم الشروحات هو أن هذه العملية تحدث في وقت تعريف الدالة (function definition time)، وليس وقت التنفيذ (runtime). هذا الفرق حاسم لفهم متى يمكن أن تسبب الديكوريترز مشاكل.
لنأخذ مثالاً بسيطاً: ديكوريتر لقياس وقت التنفيذ. معظم الأمثلة على الإنترنت تستخدم time.time()، لكن هذا غير دقيق في الأنظمة متعددة النواة بسبب انحراف الساعة. الحل الاحترافي هو استخدام time.perf_counter() الذي يعتمد على عداد المعالج. لكن حتى هذا يمكن أن يكون مضللاً إذا لم تفهم أن الديكوريتر نفسه يضيف overhead. مثلاً، إذا كانت الدالة الأصلية تستغرق ١٠ ميكروثانية، والدالة التي يرجعها الديكوريتر تستغرق ٥ ميكروثانية إضافية، فهذا يعني زيادة بنسبة ٥٠٪ في وقت التنفيذ. في الأنظمة عالية الأداء، هذه النسبة يمكن أن تكون كارثية.
import time
from functools import wraps
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.6f} seconds")
return result
return wrapper
@timing_decorator
def process_data(data):
# محاكاة معالجة بيانات تستغرق وقتاً
time.sleep(0.1)
return sum(x * 2 for x in data)
# اختبار الأداء
if __name__ == "__main__":
data = list(range(10000))
for _ in range(5):
process_data(data)لاحظ استخدام @wraps(func) هنا. هذا ليس مجرد تجميل، بل يحافظ على metadata الدالة الأصلية مثل __name__ و __doc__، وهو أمر حيوي للـ debugging وأدوات مثل Sphinx لتوليد الوثائق. بدون @wraps، ستظهر رسائل الخطأ باسم wrapper بدلاً من اسم الدالة الأصلية، وهذا يجعل تتبع الأخطاء كابوساً في بيئات الإنتاج.
في مشاريع الإنتاج الكبيرة، من الشائع أن ترى دوالاً مزينة بثلاثة أو أربعة ديكوريترز متداخلة. مثلاً، دالة API قد تكون مزينة بـ @retry، @auth_required، @log_request، و @rate_limit. المشكلة أن كل ديكوريتر يضيف طبقة جديدة من الـ function call overhead، وإذا لم تكن حذراً، يمكن أن يؤدي هذا إلى تباطؤ كبير. مثلاً، في مشروع سابق، وجدنا أن دالة بسيطة تستغرق ٥٠ ميكروثانية بدون ديكوريترز، أصبحت تستغرق ٣٥٠ ميكروثانية مع أربعة ديكوريترز متداخلة - زيادة بنسبة ٦٠٠٪!
الحل؟ أولاً، قم بقياس الأداء دائماً. استخدم أدوات مثل cProfile أو py-spy لتحديد أي ديكوريتر يسبب التباطؤ. ثانياً، فكر في دمج الديكوريترز المتشابهة في ديكوريتر واحد. مثلاً، بدلاً من استخدام @log_request و @retry بشكل منفصل، يمكنك إنشاء ديكوريتر واحد يجمع بين الاثنين. ثالثاً، استخدم الديكوريترز فقط عندما تكون هناك حاجة حقيقية. مثلاً، إذا كنت تستخدم @retry فقط في دالة واحدة، ربما من الأفضل كتابة منطق الـ retry داخل الدالة نفسها بدلاً من استخدام ديكوريتر.
from functools import wraps
import time
import random
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying in {delay} second(s)...")
time.sleep(delay)
return wrapper
return decorator
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print(f"Executing {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end - start:.4f} seconds")
return result
return wrapper
# ديكوريتر مركب
retry_and_log = lambda func: retry()(log_execution(func))
@retry_and_log
@retry(max_attempts=2, delay=0.5) # هذا سيُهمل بسبب الديكوريتر المركب أعلاه
def unreliable_function(x):
if random.random() < 0.7:
raise ValueError("Random failure!")
return x * 2
# اختبار
for i in range(5):
try:
print(unreliable_function(i))
except Exception as e:
print(f"Failed after retries: {e}")أحد أخطر المشاكل التي يمكن أن تسببها الديكوريترز هو الـ memory leak، خاصة عندما تحتفظ الديكوريترز بمراجع للدوال أو البيانات. مثلاً، إذا كان لديك ديكوريتر يحتفظ بقائمة من نتائج الدوال المزينة، وهذا الديكوريتر يُستخدم في دالة تُستدعى ملايين المرات، ستجد أن الذاكرة تُستهلك بسرعة كبيرة. في أحد المشاريع، اكتشفنا أن ديكوريتر لتسجيل الأخطاء كان يحتفظ بقائمة بكل الاستثناءات التي حدثت، مما أدى إلى استهلاك ٤ جيجابايت من الذاكرة بعد يوم واحد من التشغيل.
الحل؟ استخدم weakref عندما تحتاج إلى الاحتفاظ بمراجع للدوال أو الكائنات. مثلاً، بدلاً من الاحتفاظ بقائمة من الدوال، استخدم weakref.WeakSet. أيضاً، تجنب الاحتفاظ ببيانات كبيرة داخل الديكوريتر نفسه. إذا كنت بحاجة إلى تخزين حالة، استخدم متغيرات محلية داخل الدالة التي يرجعها الديكوريتر، أو استخدم كائنات خارجية مع آليات تنظيف مناسبة.
import weakref
from functools import wraps
class ErrorTracker:
def __init__(self):
self._errors = weakref.WeakSet()
def track_errors(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
self._errors.add(e)
print(f"Error occurred: {e}")
raise
return wrapper
def get_error_count(self):
return len(self._errors)
# استخدام الديكوريتر
tracker = ErrorTracker()
@tracker.track_errors
def risky_operation(x):
if x % 3 == 0:
raise ValueError("x is divisible by 3")
return x * 10
# اختبار
for i in range(10):
try:
print(risky_operation(i))
except:
pass
print(f"Total errors tracked: {tracker.get_error_count()}")في أوبر، يستخدمون ديكوريترز لإدارة الـ rate limiting على مستوى الدوال الفردية. بدلاً من كتابة منطق الـ rate limiting في كل دالة، لديهم ديكوريتر مثل @rate_limit(max_calls=100, period=60) الذي يضمن ألا تُستدعى الدالة أكثر من ١٠٠ مرة في الدقيقة. هذا النهج قلل من تكرار الكود بنسبة ٨٠٪ في خدماتهم الداخلية، ووفّر عليهم ساعات من الـ debugging عندما احتاجوا لتغيير سياسة الـ rate limiting في جميع الخدمات.
في سبوتيفاي، يستخدمون ديكوريترز لتتبع أداء الـ endpoints في خدماتهم الميكروسيرفس. لديهم ديكوريتر اسمه @performance_tracker يرسل بيانات الأداء إلى نظام مراقبة مركزي. هذا يسمح لهم بتحديد الـ endpoints البطيئة بسرعة واتخاذ إجراءات لتحسينها. قبل استخدام الديكوريترز، كانوا يضطرون لإضافة كود التتبع يدوياً في كل endpoint، مما أدى إلى أخطاء وعدم اتساق في البيانات.
# مثال مبسط لديكوريتر rate limiting مشابه لما تستخدمه أوبر
from functools import wraps
import time
from collections import defaultdict
class RateLimiter:
def __init__(self):
self._calls = defaultdict(list)
def rate_limit(self, max_calls, period):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# إزالة المكالمات القديمة
self._calls[func].append(now)
self._calls[func] = [
t for t in self._calls[func]
if now - t < period
]
if len(self._calls[func]) > max_calls:
raise Exception(f"Rate limit exceeded for {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
# استخدام الديكوريتر
limiter = RateLimiter()
@limiter.rate_limit(max_calls=3, period=10)
def api_endpoint(user_id):
print(f"Processing request for user {user_id}")
return {"status": "success"}
# اختبار
for i in range(5):
try:
print(api_endpoint(i))
except Exception as e:
print(e)
time.sleep(2)عندما بدأت بايثون تدعم الـ async/await، ظن الكثيرون أن الديكوريترز ستعمل بنفس الطريقة مع الدوال غير المتزامنة. لكن الحقيقة أكثر تعقيداً. المشكلة الرئيسية هي أن الديكوريتر العادي لا يمكنه التعامل مع الدوال غير المتزامنة بشكل صحيح. مثلاً، إذا استخدمت ديكوريتر عادي مع دالة async، ستجد أن الدالة لا تنتظر الـ await داخلها، وهذا يمكن أن يؤدي إلى سلوك غير متوقع أو أخطاء صعبة التتبع.
الحل هو استخدام ديكوريترز خاصة للدوال غير المتزامنة. يجب أن تكون الدالة التي يرجعها الديكوريتر غير متزامنة أيضاً، ويجب استخدام await عند استدعاء الدالة الأصلية. أيضاً، يجب أن تكون حذراً مع الـ event loop. مثلاً، إذا كان لديك ديكوريتر ينفذ كوداً متزامناً كثيفاً داخل دالة غير متزامنة، ستجد أن الـ event loop يتوقف، وهذا يمكن أن يسبب مشاكل كبيرة في التطبيقات التي تعتمد على الـ async I/O.
import asyncio
from functools import wraps
def async_timing_decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = asyncio.get_event_loop().time()
result = await func(*args, **kwargs)
end = asyncio.get_event_loop().time()
print(f"{func.__name__} took {end - start:.6f} seconds")
return result
return wrapper
def async_retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return await func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying in {delay} second(s)...")
await asyncio.sleep(delay)
return wrapper
return decorator
@async_timing_decorator
@async_retry(max_attempts=2, delay=0.5)
async def fetch_data(url):
# محاكاة طلب HTTP غير موثوق
await asyncio.sleep(0.2)
if hash(url) % 3 == 0:
raise ConnectionError("Failed to fetch data")
return {"data": f"content from {url}"}
# اختبار
async def main():
urls = ["https://example.com/1", "https://example.com/2", "https://example.com/3"]
tasks = [fetch_data(url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptiTrue)
for result in results:
if isinstance(result, Exception):
print(f"Error: {result}")
else:
print(result)
asyncio.run(main())رغم كل الفوائد، هناك حالات يجب فيها تجنب الديكوريترز تماماً. أولاً، إذا كنت تعمل في نظام حساس للأداء مثل الألعاب أو الأنظمة المالية عالية التردد، فإن الـ function call overhead الذي تضيفه الديكوريترز يمكن أن يكون غير مقبول. مثلاً، في أحد مشاريع التداول عالي التردد، وجدنا أن إزالة ديكوريتر واحد قلل زمن الاستجابة بنسبة ١٥٪، وهذا فرق كبير في هذا المجال.
ثانياً، إذا كان الديكوريتر معقداً جداً، فمن الأفضل كتابته ككود عادي داخل الدالة. مثلاً، إذا كان لديك ديكوريتر يحتوي على ٥٠ سطراً من المنطق المعقد، فهذا يجعل الكود أصعب في الفهم والصيانة. قاعدة الإبهام: إذا كان الديكوريتر أطول من الدالة التي يزينها، فمن الأفضل دمج المنطق داخل الدالة نفسها. ثالثاً، تجنب الديكوريترز عندما تحتاج إلى تعديل سلوك الدالة بناءً على حالة خارجية متغيرة باستمرار. مثلاً، إذا كان سلوك الديكوريتر يعتمد على متغير خارجي يتغير في كل استدعاء، فمن الأفضل استخدام نمط Strategy بدلاً من الديكوريتر.
الديكوريترز في بايثون ليست مجرد أداة تجميلية، بل هي سلاح هندسي قوي يمكن أن يحل مشاكل حقيقية في الإنتاج - إذا استخدمتها بحكمة. القاعدة الذهبية التي أتبعها في جميع مشاريعي: "إذا كنت تستخدم نفس الكود في ثلاث دوال أو أكثر، فكر في تحويله إلى ديكوريتر". لكن لا تنسَ قياس الأداء دائماً. استخدم أدوات مثل cProfile و py-spy لتحديد تأثير الديكوريترز على زمن التنفيذ واستهلاك الذاكرة. وإذا وجدت أن ديكوريتر معين يسبب مشاكل، فلا تتردد في إزالته واستبداله بكود عادي. في النهاية، الهدف هو كتابة كود نظيف وسريع وسهل الصيانة، وليس استخدام الميزات اللغوية لمجرد استخدامها.
ابدأ اليوم بإنشاء ديكوريتر واحد لحل مشكلة حقيقية في مشروعك الحالي. سواء كان لتسجيل الأخطاء، أو قياس الأداء، أو إدارة الـ rate limiting، ستجد أن الديكوريترز يمكن أن تجعل كودك أكثر أناقة وكفاءة. فقط تذكر: الديكوريترز مثل التوابل في الطبخ - القليل يضيف نكهة رائعة، والكثير يدمر الطبق.
الديكوريترز في بايثون هي مثل السحر الأسود: إذا لم تفهم كيف تعمل، ستسبب لك الكوابيس. لكن إذا أتقنتها، ستصبح ساحراً حقيقياً.
— مطور مجهول في مؤتمر بايثون أوروبا ٢٠٢٢