جملة with في بايثون: الأداة التي تنظّف وراءك تلقائياً
أي مورد تفتحه داخل برنامجك — سواء كان ملفاً، أو اتصالاً بالشبكة، أو جلسة قاعدة بيانات، أو قفلاً — لا بد من إغلاقه بعد الانتهاء منه. وإن نسيت ذلك، فستتسرّب الذاكرة، أو تبقى الأقفال محتجزة فتعطّل عمليات أخرى، أو تتلف الملفات عند حدوث أي انهيار. وهنا يأتي دور جملة with في بايثون لتتكفّل بكل هذا نيابةً عنك.
وأشهر نمط يبدأ به الجميع هو قراءة الملفات في بايثون:
with open("notes.txt") as f:
contents = f.read()
print(contents)
يحدث شيئان تلقائياً. عند الدخول، تُرجع open() كائن ملف مربوطاً بالاسم f. وعند الخروج — سواءً انتهت الكتلة بشكل طبيعي، أو خرجت مبكراً عبر return، أو رُفع استثناء — يستدعي بايثون f.close() نيابةً عنك.
هذا كل شيء. هذه هي فكرة with باختصار.
ما الذي تحلّ محلّه جملة with؟
قبل ظهور مديرات السياق، كانت الطريقة الآمنة المكافئة تعتمد على try/finally:
f = open("notes.txt")
try:
contents = f.read()
print(contents)
finally:
f.close()
خمسة أسطر من البيروقراطية لمجرد "افتح ملف واقرأ محتواه وأغلقه في النهاية". تخيّل أن تضرب هذا في كل استدعاء لـ open داخل برنامج كبير نسبياً، وستفهم لماذا يلجأ الجميع إلى جملة with. فهي أقصر، وأقل عرضة للخطأ، ومن المستحيل أن تنسى معها إغلاق الموارد.
فتح أكثر من مورد في جملة with واحدة
تتيح لك بايثون ربط عدة مديري سياق داخل جملة with واحدة:
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
كلا الملفين يُفتحان عند الدخول، ويُغلقان عند الخروج. وإذا نجحت عملية open الأولى ثم فشلت الثانية وأطلقت استثناءً، فإن بايثون سيُغلق الملف الأول رغم ذلك — فالآلية الداخلية تتعامل مع حالات التهيئة الجزئية كما ينبغي.
أما إذا كان لديك عدد أكبر من الموارد، فصيغة الأقواس (المتاحة في بايثون 3.10 فأحدث) تكون أوضح:
with (
open("a.txt") as a,
open("b.txt") as b,
open("c.txt") as c,
):
...
ما هو مدير السياق في بايثون فعلياً؟
أي كائن يُعرِّف التابعين __enter__ و __exit__ يُعتبر مدير سياق (Context Manager). البروتوكول بسيط جداً:
__enter__(self)يُنفَّذ عند بداية كتلةwith، والقيمة التي يُرجعها هي ما يرتبط بالاسم بعدas.__exit__(self, exc_type, exc_value, traceback)يُنفَّذ عند انتهاء الكتلة، مهما كانت طريقة الخروج. وإذا كان سبب الخروج استثناءً، فإن تفاصيله تُمرَّر إليه حتى يستطيع مدير السياق فحصها أو حتى كبتها.
إليك أبسط مثال ممكن، مدير سياق يقيس زمن تنفيذ الكتلة التي يُغلِّفها:
with Timer(): يُنشئ الكائن، ثم يستدعي __enter__، ثم ينفّذ جسم الكتلة، وأخيراً يستدعي __exit__. لا ملف هنا ولا قفل — مجرد غلاف بسيط حول فكرة "نفّذ شيئاً وقِس الزمن الذي استغرقه".
اختصار contextlib.contextmanager
تعريف صنف كامل لكل مدير سياق أمر مُرهق أكثر مما ينبغي. هنا يأتي دور contextlib.contextmanager، إذ يحوّل دالة مولِّدة (generator) إلى مدير سياق جاهز — وتفصل كلمة yield واحدة بين ما يحدث "قبل" وما يحدث "بعد":
كل ما يسبق yield يمثّل سلوك __enter__، وكل ما يأتي بعده يمثّل __exit__. وجود try/finally يضمن تنفيذ خطوة التنظيف حتى لو أطلق جسم الكتلة استثناءً.
غالبية مديري السياق المخصصين الذين ستكتبهم في بايثون تنطبق عليهم هذه الصيغة. ابدأ دائماً بصيغة الديكوراتور، ولا تلجأ إلى الصنف (class) إلا حين تحتاج شيئاً لا يمكن للمولّد (generator) التعبير عنه.
تغيير قيمة بشكل مؤقت
نمط شائع جداً: نضبط قيمة، ونستخدمها، ثم نعيدها إلى ما كانت عليه. مدير السياق في بايثون يعبّر عن هذا النمط بأسلوب أنيق:
أي نمط من أسلوب "عيّن القيمة ثم استعدها" — سواء كان لمتغيرات البيئة، أو مستوى تفصيل سجلات الـ logging، أو رايات الميزات (feature flags)، أو تجهيزات الاختبارات — يتناسب بشكل طبيعي مع مدير السياق في بايثون. ولن يحتاج من يستدعي الكود أن يتذكر إعادة أي شيء إلى حالته الأصلية.
ابتلاع الاستثناءات داخل مدير السياق
يمكن لدالة __exit__ أن تُرجع True لتُخبر بايثون: "لقد تعاملت مع الاستثناء، فتجاهله." هذا الأمر نادر الاستخدام وعادةً ما يكون مؤشرًا على تصميم غير سليم، لكنه الآلية التي تعتمد عليها contextlib.suppress:
suppress(FileNotFoundError) يحوّل استثناء FileNotFoundError إلى عملية بلا أثر، أي يتم تجاهله تمامًا. استخدمه فقط مع العمليات الاختيارية فعلًا، بمعنى "جرّب هذا، ولا يهمّ إن فشل". لكن لا تلجأ إليه لإسكات استثناءات لم تفكّر بها جيدًا.
مديرو سياق آخرون ستصادفهم
بمجرّد أن تبدأ بالانتباه، ستجد أن مديري السياق في بايثون منتشرون في كل زاوية من المكتبة القياسية:
import threading
from pathlib import Path
# Locks — guarantee release even if the critical section raises.
lock = threading.Lock()
with lock:
...
# tempfile — delete the temp file when you're done.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
path = Path(tmp) / "scratch.txt"
path.write_text("hello")
# Database connections — close the connection (or end the transaction).
import sqlite3
with sqlite3.connect(":memory:") as conn:
conn.execute("CREATE TABLE t (x INTEGER)")
المكتبات الخارجية تتبع نفس الاصطلاح. لمّا تشوف with something as x:، هذا يعني تقريباً دائماً: "استخدم x داخل هذا البلوك، وبعدها نظّف كل شيء تلقائياً".
متى لا تستخدم جملة with؟
- لمّا ما يكون عندك فعلاً مرحلة تهيئة وتنظيف. تغليف كود عادي بمدير سياق بدون سبب يضيف تشويشاً لا فائدة منه.
- لمّا تحتاج المورد في عدة بلوكات غير مترابطة. إبقاء
withمفتوحة طوال عمر سكربت طويل يُخفي النطاق الحقيقي لعملية التنظيف. في هذه الحالة، الأفضل تستخدم كلاساً يملك المورد. - لمّا يكون الـ decorator أنسب. بعض الأنماط المتكررة (مثل إعادة المحاولة، التسجيل، قياس الوقت) تُقرأ بشكل أطبع لمّا تكون
@decoratorفوق الدالة بدلاً منwith ...:داخلها. اختر الأسلوب الأوضح في موضع الاستدعاء.
في معظم الحالات، with هي الخيار الصحيح. والاستثناءات النادرة يسهل اكتشافها لمّا تعرف كيف تبحث عنها.
الخطوة التالية: التعامل مع ملفات حقيقية
الآن صرت فاهماً الآلية اللي تشتغل خلف with open(...) as f: — وهذا هو السياق اللي راح تستخدمه فيه في تسعين بالمئة من الحالات. الفصل القادم يوظّف هذه المعرفة عملياً في قراءة الملفات والكتابة فيها والتنقّل داخلها على القرص.
الأسئلة الشائعة
ماذا تفعل with open في بايثون؟
with open في بايثون؟الجملة with open(path) as f: تفتح الملف وتربطه بالمتغير f طوال فترة تنفيذ الكتلة البرمجية. وعند انتهاء الكتلة — سواء بشكل طبيعي أو بسبب استثناء — يقوم بايثون بإغلاق الملف تلقائياً. لست بحاجة لاستدعاء f.close() بنفسك، فجملة with تضمن لك ذلك.
لماذا نستخدم with بدلاً من open() المجرّدة؟
with بدلاً من open() المجرّدة؟لأن with تُغلق الملف حتى لو حدث استثناء في منتصف تنفيذ الكتلة. أما open() العادية فتُحمِّلك مسؤولية تذكّر close() في كل مسار من مسارات التنفيذ، بما فيها مسارات الخطأ. باختصار: with أأمن وأقصر.
كيف أفتح عدة ملفات داخل جملة with واحدة؟
with واحدة؟افصل بين مدراء السياق بفاصلة، هكذا: with open('a.txt') as a, open('b.txt') as b:. يُفتح الملفان عند الدخول ويُغلقان عند الخروج بالترتيب العكسي. هذه الطريقة تُغنيك عن تداخل جُمَل with عندما تحتاج أكثر من مورد في الوقت ذاته.