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

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

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

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

المنصة

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

الدعم

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

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

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

Python Context Managers للمتوسط: استبدل try/finally بـ with وضمن غلق الموارد

📅 ١٨ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Python Context Managers للمتوسط: استبدل try/finally بـ with وضمن غلق الموارد

مستوى القارئ: متوسط. المقال ده مكتوب لمن بيكتب Python بانتظام ويعرف try/except/finally ومحتاج يكسب نظافة كود و resource safety. لو لسه مبتدئ، ابدأ من قسم "الـ Context Manager بمثال حارس الباب" وارجع لباقي الأقسام بعد كده.

لو الـ service بتاعتك بتفتح ملف أو PostgreSQL connection في 22 مكان مختلف، وكل مكان معمول بـ try/finally، انت بتعيد كتابة 6 سطور boilerplate في كل مرة. وكفاية تنسى finally في مكان واحد عشان السيرفر يبدأ ياكل file descriptors لحد ما يقف.

شاشة محرر كود تعرض ملف Python يحتوي على with statement لإدارة موارد الملفات

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

عندك دالة بتفتح ملف، تكتب فيه، تقفله. لو exception حصل بين الفتح والقفل، الملف بيفضل مفتوح في الذاكرة لحد ما الـ garbage collector يلاقيه. على سيرفر بيخدم 8,400 طلب/دقيقة، ده بيتراكم في ثواني.

الافتراض هنا إنك بتشتغل على CPython 3.10+ وبتستخدم مكتبات بتدعم الـ context manager protocol (وده الحال في 95% من المكتبات الشائعة: open()، psycopg2، requests.Session، threading.Lock، tempfile.TemporaryDirectory).

الـ Context Manager بمثال حارس الباب

تخيّل غرفة فيها سيرفر مهم. أي حد يدخل لازم يقفل الباب لما يخرج، وإلا أي حد تاني يقدر يدخل ويعبث. كل مرة تنسى تقفل، المشكلة بتكبر.

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

الـ context manager في Python هو الحارس ده بالظبط. أي object فيه method اسمها __enter__ و method اسمها __exit__ يبقى context manager. والكلمة with هي اللي بتقول للـ Python "نادي الحارس ده".

شبكة أنابيب وصمامات تفتح وتغلق تمثل دورة حياة الموارد في الـ context manager

التعريف العلمي الدقيق

الـ Context Manager Protocol موصوف رسمياً في PEP 343 (Guido van Rossum و Nick Coghlan، 2005). البيان with EXPR as VAR: بيتـ desugar تحت الكابوت لما يلي:

Python
manager = EXPR
exit = type(manager).__exit__
value = type(manager).__enter__(manager)
exc = True
try:
    try:
        VAR = value
        BLOCK
    except:
        exc = False
        if not exit(manager, *sys.exc_info()):
            raise
finally:
    if exc:
        exit(manager, None, None, None)

الـ الضمانة هنا إن __exit__ بيتنادى دايماً: في الحالة العادية، في الحالة اللي فيها exception، حتى لو في return أو break جوّه الـ block. ده اللي بيخلّيه أأمن من try/finally اليدوي — لأنه impossible تنساه.

قبل وبعد: 11 سطر بدل 4

الطريقة القديمة بـ try/finally nested:

Python
import psycopg2

def get_order(order_id):
    conn = psycopg2.connect(dsn)
    try:
        cur = conn.cursor()
        try:
            cur.execute(
                "SELECT * FROM orders WHERE id = %s",
                (order_id,)
            )
            return cur.fetchone()
        finally:
            cur.close()
    finally:
        conn.close()

نفس الكود بـ with:

Python
import psycopg2

def get_order(order_id):
    with psycopg2.connect(dsn) as conn:
        with conn.cursor() as cur:
            cur.execute(
                "SELECT * FROM orders WHERE id = %s",
                (order_id,)
            )
            return cur.fetchone()

أو حتى أنضف، بـ with متعدد على سطر واحد (متاح من Python 3.10):

Python
def get_order(order_id):
    with (
        psycopg2.connect(dsn) as conn,
        conn.cursor() as cur,
    ):
        cur.execute("SELECT * FROM orders WHERE id = %s", (order_id,))
        return cur.fetchone()

اكتب context manager خاص بيك في 12 سطر

فيه طريقتين: class-based (للحالات المعقّدة) و @contextmanager decorator (الأسرع والأنظف للحالات البسيطة).

الطريقة الأولى: @contextmanager (مفضّلة)

Python
from contextlib import contextmanager
import time
import logging

log = logging.getLogger(__name__)

@contextmanager
def timed(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed_ms = (time.perf_counter() - start) * 1000
        log.info("%s took %.2fms", label, elapsed_ms)

with timed("aggregate_report"):
    result = run_heavy_query()

الكود اللي قبل yield بيشتغل في __enter__، واللي بعده في __exit__. لازم تحط try/finally حول الـ yield عشان الـ cleanup يحصل حتى مع exception.

الطريقة الثانية: class-based

Python
class DatabaseTransaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.cur = self.conn.cursor()
        return self.cur

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.conn.commit()
        else:
            self.conn.rollback()
        self.cur.close()
        return False  # متبلعش الـ exception

with DatabaseTransaction(conn) as cur:
    cur.execute("INSERT INTO orders ...")
    cur.execute("UPDATE inventory ...")

ركّز في الـ return False في آخر __exit__. لو رجّعت True، الـ exception هيتبلع في صمت. ده الـ trap رقم واحد اللي بيوقع فيه الناس.

سيناريو واقعي بالأرقام

خدمة fintech عربية بتعالج 8,400 طلب/دقيقة. الفريق كان بيفتح PostgreSQL connection يدوي في 22 مكان، 4 منهم فيه except بيرجع early بدون finally صحيح. النتيجة:

  • بعد 14 يوم من الـ deploy: 187 idle connection ميت في PostgreSQL من أصل max_connections=200.
  • الـ P95 latency على endpoint /orders طلع من 78ms لـ 1,840ms بسبب connection starvation.
  • كل deploy جديد كان بيـ "يحل" المشكلة لأنه بيـ restart الـ workers.

بعد تحويل الـ 22 مكان لـ with: عدد الـ idle connections الميتة = 0 على 90 يوم متواصلين، الـ P95 رجع لـ 82ms، والـ restarts الـ "علاجية" اتلغت من الـ runbook.

أربع trade-offs خفية لازم تعرفها

  1. Async مختلف. لو شغّال على asyncio، الـ with العادي مش هيشتغل على async resources. لازم async with والـ class لازم يـ implement __aenter__ و __aexit__. الفرق بسيط في الـ syntax لكن الـ runtime مختلف تماماً.
  2. __exit__ يقدر يبلع exceptions. لو رجع truthy value، Python بيعتبر الـ exception اتعالج وبيكمل عادي. ده مفيد في حالات نادرة (زي suppress الـ FileNotFoundError في contextlib.suppress) لكن خطير لو حصل بالغلط.
  3. Overhead صغير لكن موجود. كل دخول/خروج context manager بياخد حوالي 0.4–0.8 ميكروثانية على CPython 3.12 على CPU حديث. في hot loop بـ مليون iteration، ده بيبقى نصف ثانية. مش مشكلة في 99% من الحالات، لكن في tight loops استخدم try/finally يدوي.
  4. الـ value من __enter__ مش لازم نفس الـ object. open() بترجع نفس الـ file object، بس psycopg2.connect() بترجع الـ connection نفسه (مش cursor)، و contextlib.suppress بترجع None. اقرا توثيق المكتبة قبل ما تفترض.

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

  • Resource مفروض يفضل مفتوح بعد scope الدالة. زي connection pool global بيستخدمه التطبيق كله — مفيش "خروج" تنادي عنده cleanup.
  • الـ resource مفهوش cleanup logic أصلاً. ولو ضفت __enter__/__exit__ فاضيين، انت بتعمل ضوضاء بدون فايدة.
  • الـ lifecycle بتاع الـ resource مش متطابق مع الـ block. مثلاً، لو محتاج تفتح ملف هنا وتقفله في callback تاني، الـ with مش هينفع — استخدم try/finally أو contextlib.ExitStack.
  • Hot path بـ Python بـ مليون iteration. كما ذكرنا في الـ trade-offs، الـ overhead الـ 0.4μs بيتراكم.

أداة متقدمة: ExitStack لإدارة عدد متغير من الموارد

لو محتاج تفتح N ملف، و N مش معروف وقت الكتابة:

Python
from contextlib import ExitStack

def merge_files(paths, output):
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        with open(output, 'w') as out:
            for f in files:
                out.write(f.read())

الـ ExitStack بيضمن إن كل الملفات اللي اتفتحت هتتقفل، حتى لو الـ 17th file فشل في الفتح. الموارد اللي اتفتحت قبله هتتقفل بالترتيب العكسي (LIFO).

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

افتح أكبر ملف Python في مشروعك دلوقتي وابحث عن النمط ده: try: ... finally: ... .close(). لو لقيت أكثر من 3 occurrences، حوّلهم لـ with. لو الـ class بتاعك بيدير resource (file, connection, lock, lock-like object)، ضيفله __enter__ و __exit__ في 4 سطور. وابعتلي اللي اتغيّر معاك.

مصادر

  • PEP 343 — The "with" Statement (Guido van Rossum, Nick Coghlan, 2005) — peps.python.org/pep-0343
  • Python Language Reference, Section 8.5 — "The with statement" — docs.python.org/3/reference/compound_stmts.html#with
  • contextlib module documentation — docs.python.org/3/library/contextlib.html
  • "Fluent Python" 2nd Edition — Luciano Ramalho، الفصل 18 "with, match, and else Blocks" (O'Reilly, 2022)
  • PEP 617 — New PEG parser for CPython (للسياق حول دعم الـ parenthesized context managers في 3.10)

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

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

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