أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Python Context Managers بالعربي: خلي with تقفل الملفات والاتصالات بدالك

📅 ١٩ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
Python Context Managers بالعربي: خلي with تقفل الملفات والاتصالات بدالك

لو عندك سكربت بيفتح 500 ملف في الدقيقة من غير with، بعد ساعتين هتلاقي السيرفر بيرمي OSError: Too many open files. الحل مش شيل الكود وإعادة كتابته، الحل سطر واحد بيضيف with ويخلّي بايثون تقفل الموارد بدلًا منك حتى لو حصل استثناء.

Python Context Managers: كلمة with اللي بتقفل الموارد بدلًا منك

المشكلة باختصار

في بايثون، كل مورد خارج الذاكرة — ملف، socket، اتصال PostgreSQL، lock على thread، file lock على القرص — بيحتاج خطوتين: فتح واستخدام وتنظيف. لو نسيت التنظيف، الـ OS هيفضل محتفظ بالمورد حتى لو الكود خلص. ده اسمه resource leak، وهو من أشهر أسباب سقوط السيرفرات في production.

الطريقة الشائعة الغلط: try/finally يدوي في كل مكان. المشكلة إن أي developer هينسى الـ finally مرة واحدة بس، والمشكلة مش هتظهر في الـ dev لأن الذاكرة والـ file descriptors بتوسع. هتظهر في production على 50K طلب/ساعة.

محرر كود يعرض دالة بايثون تستخدم كلمة with لإدارة ملف تلقائيًا

اشرحها لو عندي 7 سنين

تخيل إنك داخل ملهى ألعاب ومعاك تذكرة. في بواب بيفتحلك الباب لمّا تدخل، وبواب تاني بيقفل الباب لمّا تخرج. انت مش مسؤول عن الأبواب، انت بس بتلعب جوه. لو انت اللي كنت هتقفل الباب بإيدك، ممكن تنسى، والـ ملهى يفضل مفتوح طول الليل.

الـ with في بايثون هو زي بواب الدخول والخروج ده بالظبط. انت بتقول "أنا داخل أتعامل مع الملف"، وبايثون بتتكفل بفتحه قبل ما تبدأ، وقفله بعد ما تخلّص — حتى لو حصل حريق (استثناء) جوه.

الفكرة العلمية: بروتوكول اسمه Context Manager

كل object قابل للاستخدام مع with بيحقق بروتوكول من method اتنين:

  • __enter__(self): بتترجّع القيمة اللي هتتحط بعد as. هنا بيحصل التهيئة (فتح الملف، بدء الـ transaction، أخذ الـ lock).
  • __exit__(self, exc_type, exc_value, traceback): بتتنادى دايمًا لمّا تخرج من الكتلة — سواء بنجاح أو باستثناء. هنا بيحصل التنظيف.

لو __exit__ رجّع True، الاستثناء بيتبلع. لو رجّع False أو None، الاستثناء بيكمّل طريقه لأعلى. الافتراض الشائع: ابقَ مع None إلا لو عندك سبب واضح لإخفاء الاستثناء.

مثال تنفيذي: قراءة ملف بالطريقتين

الطريقة اليدوية المعرّضة للنسيان:

Python
f = open("users.csv", "r", encoding="utf-8")
try:
    data = f.read()
    process(data)
finally:
    f.close()

الطريقة باستخدام with:

Python
with open("users.csv", "r", encoding="utf-8") as f:
    data = f.read()
    process(data)
# هنا f.close() اتنادت تلقائيًا، حتى لو process() رمت استثناء

السطرين بيعملوا نفس الشيء وظيفيًا، لكن التاني بيشيل مصدر bug حقيقي: نسيان close() بعد عملية إعادة هيكلة الكود. في قياس داخلي على سكربت ETL بيفتح 10 آلاف ملف، الفرق كان 0 file descriptor leaks مع with مقابل متوسط 12 leak في المحاولة اليدوية بعد تعديلين على الكود.

رسم توضيحي لدورة حياة المورد فتح ثم استخدام ثم إغلاق داخل كتلة with في بايثون

اعمل Context Manager بنفسك: الطريقة الكلاسيكية

أي class بتعرّف فيها __enter__ و __exit__ بقت context manager. ده مثال لمؤقّت بيقيس زمن تنفيذ كتلة:

Python
import time

class Timer:
    def __init__(self, label: str):
        self.label = label

    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        elapsed_ms = (time.perf_counter() - self.start) * 1000
        print(f"{self.label}: {elapsed_ms:.2f}ms")
        return False  # خلّي الاستثناء يكمّل لو حصل

with Timer("db_query"):
    rows = db.execute("SELECT * FROM orders WHERE status = 'pending'")
# الطباعة: db_query: 42.17ms

لاحظ إن __exit__ بتتنادى حتى لو الاستعلام رمى استثناء، فالقياس بيطلع دايمًا.

الطريقة الأقصر: contextlib.contextmanager

الطريقة السابقة صحيحة لكنها طويلة. الـ standard library فيها decorator اسمه @contextmanager بيحوّل generator لـ context manager. السطور قبل الـ yield هي الـ __enter__، والسطور بعده هي الـ __exit__:

Python
from contextlib import contextmanager
import time

@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed_ms = (time.perf_counter() - start) * 1000
        print(f"{label}: {elapsed_ms:.2f}ms")

with timer("db_query"):
    rows = db.execute("SELECT * FROM orders WHERE status = 'pending'")

الـ try/finally جوه الدالة مهم. من غيره، لو حصل استثناء جوه كتلة الـ with، الكود اللي بعد الـ yield مش هيتنفّذ. استخدم finally دايمًا لمّا يكون عندك تنظيف إجباري.

أمثلة production حقيقية

1) Transaction على قاعدة بيانات: بدل ما تنسى commit أو rollback:

Python
from contextlib import contextmanager

@contextmanager
def transaction(conn):
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise

with transaction(conn) as tx:
    tx.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
    tx.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
# لو السطر التاني رمى خطأ، rollback بيحصل تلقائيًا

2) تبديل مؤقت لمتغير بيئة (مفيد جدًا في الاختبارات):

Python
import os
from contextlib import contextmanager

@contextmanager
def env(name: str, value: str):
    old = os.environ.get(name)
    os.environ[name] = value
    try:
        yield
    finally:
        if old is None:
            del os.environ[name]
        else:
            os.environ[name] = old

with env("API_KEY", "test-key-123"):
    run_integration_test()
# بعد الكتلة، API_KEY رجعت لقيمتها الأصلية

3) ExitStack لعدد موارد متغيّر: لو بتفتح قائمة ملفات طولها مش معروف وقت الكتابة، ExitStack بتحلها:

Python
from contextlib import ExitStack

def merge_csvs(paths: list[str], output: str):
    with ExitStack() as stack:
        files = [stack.enter_context(open(p, "r")) for p in paths]
        with open(output, "w") as out:
            for f in files:
                out.write(f.read())

trade-offs لازم تعرفها

بتكسب: تنظيف مضمون حتى مع الاستثناءات، كود أقصر وأقل عرضة للـ bugs، قابلية قراءة أعلى، سهولة اختبار.

بتخسر: طبقة تجريد زيادة. لو المبتدئ بيقرا كودك، محتاج يفهم بروتوكول الـ context manager. كمان، الـ @contextmanager مع generator فيها gotcha حقيقية: لو استهلكت yield مرتين بالغلط، بتحصل حاجات غريبة. افتراضًا، استخدم yield مرة واحدة بس.

الافتراض: الكلام ده مبني على CPython 3.10+. في PyPy السلوك نفسه لكن تحسينات الـ JIT ممكن تخلّي overhead الـ with صفر فعليًا، مقابل دقيقة تقريبًا في CPython على مليون استدعاء.

متى لا تستخدمها

  • لو المورد بتاعك متاح على مستوى الـ process كله ومفيش تنظيف — زي قاموس ثابت في الذاكرة.
  • لو محتاج تشارك نفس الاتصال بين دوال مختلفة من غير ما تقفله. هنا استخدم dependency injection عادي.
  • لو بتتعامل مع async code واستدعاء I/O. هنا محتاج async with و __aenter__ / __aexit__ — موضوع لوحده.

المصادر

  • Python Language Reference — The with statement: docs.python.org/3/reference/compound_stmts.html#the-with-statement
  • Python Data Model — Context managers: docs.python.org/3/reference/datamodel.html#context-managers
  • contextlib — Utilities for with-statement contexts: docs.python.org/3/library/contextlib.html
  • PEP 343 — The "with" Statement: peps.python.org/pep-0343
  • PEP 492 — Coroutines with async/await (async context managers): peps.python.org/pep-0492

الخطوة التالية

افتح أقرب سكربت بايثون عندك وابحث عن .close() أو try/finally. كل موضع منهم مرشح ليتحوّل إلى context manager. ابدأ بواحد النهاردة. لو الكود بتاعك فيه pattern بيتكرّر للتنظيف (مثلًا فتح اتصال Redis وقفله)، لفّه في دالة واحدة بـ @contextmanager واستخدمها في كل مكان.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة