مستوى القارئ: متوسط. المقال ده مكتوب لمن بيكتب Python بانتظام ويعرف try/except/finally ومحتاج يكسب نظافة كود و resource safety. لو لسه مبتدئ، ابدأ من قسم "الـ Context Manager بمثال حارس الباب" وارجع لباقي الأقسام بعد كده.
لو الـ service بتاعتك بتفتح ملف أو PostgreSQL connection في 22 مكان مختلف، وكل مكان معمول بـ try/finally، انت بتعيد كتابة 6 سطور boilerplate في كل مرة. وكفاية تنسى finally في مكان واحد عشان السيرفر يبدأ ياكل file descriptors لحد ما يقف.
المشكلة باختصار
عندك دالة بتفتح ملف، تكتب فيه، تقفله. لو 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 Protocol موصوف رسمياً في PEP 343 (Guido van Rossum و Nick Coghlan، 2005). البيان with EXPR as VAR: بيتـ desugar تحت الكابوت لما يلي:
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:
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:
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):
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 (مفضّلة)
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
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 خفية لازم تعرفها
- Async مختلف. لو شغّال على
asyncio، الـwithالعادي مش هيشتغل على async resources. لازمasync withوالـ class لازم يـ implement__aenter__و__aexit__. الفرق بسيط في الـ syntax لكن الـ runtime مختلف تماماً. - __exit__ يقدر يبلع exceptions. لو رجع truthy value، Python بيعتبر الـ exception اتعالج وبيكمل عادي. ده مفيد في حالات نادرة (زي suppress الـ
FileNotFoundErrorفيcontextlib.suppress) لكن خطير لو حصل بالغلط. - Overhead صغير لكن موجود. كل دخول/خروج context manager بياخد حوالي 0.4–0.8 ميكروثانية على CPython 3.12 على CPU حديث. في hot loop بـ مليون iteration، ده بيبقى نصف ثانية. مش مشكلة في 99% من الحالات، لكن في tight loops استخدم
try/finallyيدوي. - الـ 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 مش معروف وقت الكتابة:
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 contextlibmodule 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)