Context Managers في Python: اقفل الموارد حتى وقت الخطأ
هتعرف هنا إزاي تستخدم with في Python عشان تضمن إن الملفات والاتصالات والـ locks تتقفل حتى لو الكود وقع في النص.
مستوى القارئ: متوسط
المشكلة باختصار
الطريقة الشائعة الغلط إنك تفتح ملف أو connection وتفترض إن آخر سطر في الدالة هيقفل كل حاجة. الطريقة دي بتفشل أول ما يحصل exception قبل close(). النتيجة: file handles مفتوحة، locks مش متحررة، واتصالات قاعدة بيانات بتتراكم لحد ما التطبيق يبدأ يبطأ أو يرفض طلبات جديدة.
الافتراض إن عندك سكربت أو API صغير بيتعامل مع ملفات CSV أو تقارير أو اتصالات قصيرة بقاعدة بيانات. مش لازم تكون بتدير نظام ضخم عشان المشكلة تظهر. في Windows أو Linux، ألف loop فاشل ممكن يسيب مئات الموارد معلقة لو التنظيف مش مضمون.
مثال بسيط: الباب لازم يتقفل حتى لو حصل خطأ
ركز في المثال ده. عندك موظف بيفتح غرفة الأرشيف، ياخد ملف، وبعدها لازم يقفل الباب. لو التليفون رن أو الكهرباء قطعت، الباب برضه لازم يتقفل. الـ Context Manager بيعمل نفس الفكرة بالظبط: يفتح المورد في البداية، وينفذ شغلك، ثم ينادي جزء التنظيف حتى لو حصل خطأ.
علميًا، جملة with تستدعي __enter__() عند الدخول، وتستدعي __exit__() عند الخروج. __exit__() بتستقبل معلومات الخطأ لو حصل. تقدر تسيبه يظهر للمستدعي، أو تبتلعه في حالات ضيقة جدًا. الأفضل غالبًا إنك تنظف المورد وتسيب الخطأ يطلع.
الكود اللي يسبب المشكلة
المثال التالي متعمد يكون سيئًا. لو حصل خطأ قبل close()، الملف يفضل مفتوح لحد ما garbage collector يتصرف، وده مش ضمان هندسي تعتمد عليه.
def write_report_bad(path, rows):
f = open(path, "w", encoding="utf-8")
for row in rows:
if row == "BROKEN":
raise ValueError("invalid row")
f.write(row + "\n")
f.close()
الكود ده ممكن يعدي في الاختبار لو البيانات سليمة. اللي بيحصل فعلاً في الإنتاج إن صف واحد بايظ يوقف الدالة قبل close(). لو السكربت بيتكرر 1000 مرة، هتبدأ تشوف مشاكل مثل Too many open files أو lock على ملف مش عارف تمسحه.
أفضل طريقة: استخدم with
نفس المنطق، لكن التنظيف بقى جزء من هيكل اللغة نفسه. ده مش مجرد شكل أجمل للكود. ده عقد واضح: افتح المورد، نفذ الشغل، نظف بعد الخروج.
def write_report_good(path, rows):
with open(path, "w", encoding="utf-8") as f:
for row in rows:
if row == "BROKEN":
raise ValueError("invalid row")
f.write(row + "\n")
لو ValueError حصلت، Python هتنادي f.__exit__() داخليًا، وبالتالي الملف يتقفل. المكسب: كود أقصر وتنظيف مضمون. التكلفة: لازم تفهم إن عمر المورد محصور داخل بلوك with. لو حاولت تستخدم f بعد البلوك، أنت بتستخدم ملف مقفول.
اعمل Context Manager بنفسك
لو عايزها تدعم مورد خاص بيك، مثل temporary workspace أو lock أو connection wrapper، استخدم contextlib.contextmanager. المثال ده يقيس زمن العملية ويطبع النتيجة حتى لو الكود وقع.
from contextlib import contextmanager
from time import perf_counter
@contextmanager
def measured_step(name):
start = perf_counter()
print(f"start: {name}")
try:
yield
finally:
elapsed_ms = (perf_counter() - start) * 1000
print(f"done: {name} in {elapsed_ms:.1f}ms")
with measured_step("export-users"):
# ضع هنا استعلام قاعدة البيانات أو كتابة الملف
raise RuntimeError("database timeout")
وجود finally هنا مهم. هو الضمان إن كود التنظيف أو القياس يشتغل بعد النجاح وبعد الفشل. في سيناريو واقعي: job بيصدر 50 ألف مستخدم إلى CSV. بدون with ممكن خطأ في صفحة رقم 37 يسيب ملف مؤقت واتصال DB مفتوحين. مع Context Manager، التنظيف يحصل في نفس المكان اللي عرّفت فيه عمر المورد.
القياس و trade-off
قياس تقديري بسيط: loop يفتح 1000 ملف بدون إغلاق واضح ممكن يترك 1000 handle مفتوح مؤقتًا، حسب النظام وطريقة التنفيذ. نفس loop باستخدام with open(...) يرجع إلى صفر handles بعد كل دورة. الرقم المهم هنا مش السرعة؛ الفرق عادة أقل من 1ms لكل عملية ملف صغيرة. الرقم المهم هو الاستقرار تحت الفشل.
الـ trade-off هنا إنك بتحصر عمر المورد داخل بلوك واضح. ده ممتاز للملفات والـ locks والـ transactions القصيرة. لكنه مزعج لو عندك object عايز يعيش طول عمر التطبيق. في الحالة دي استخدم initialization واضح عند بدء التطبيق وshutdown hook صريح عند الإغلاق.
متى لا تستخدم هذه الطريقة
لا تستخدم Context Manager لمورد طويل العمر مثل connection pool رئيسي في web server لو البلوك هيتفتح ويتقفل مع كل request بدون داعي. لا تستخدمه كبديل عن retry أو circuit breaker. هو ينظف المورد، لكنه لا يعالج سبب فشل قاعدة البيانات أو الشبكة. ولا تبتلع الأخطاء داخل __exit__ إلا لو عندك سبب محدد ومقاس.
مصادر
الخطوة التالية
افتح أي سكربت Python عندك بيستخدم open() أو Lock() أو connection مؤقتة. حوّل أول حالة إلى with، ثم اختبرها بخطأ متعمد قبل آخر سطر. لو التنظيف اشتغل، كرر نفس النمط في باقي الموارد القصيرة.