هذا المقال للمستوى المتوسط — مناسب لمن يكتب Python يومياً ويتعامل مع ملفات وقواعد بيانات و sockets، ويعرف الـ exceptions بشكل أساسي.
Python Context Managers: اقفل الموارد بدون try/finally
لو الكود بتاعك مليان try/finally كل مرة بتفتح فيها ملف أو connection على PostgreSQL، إنت بتكتب 6 سطور ممكن تتحوّل لسطر واحد. الـ with statement بيضمن إن المورد بيتقفل حتى لو exception حصل في النص، وبدون أي boilerplate. ده مش syntactic sugar — ده عقد رسمي بين الـ object والـ interpreter اسمه Context Manager Protocol.
المشكلة باختصار
الكود اللي بيشتغل مع موارد خارجية (ملفات، sockets، DB connections، locks) لازم يضمن إن المورد بيتحرر حتى لو حصل خطأ. الطريقة الكلاسيكية بـ try/finally بتنفع، بس بتتكرر في كل مكان وبتفتح باب لأخطاء صامتة لو الـ developer نسي finally، أو نسي يقفل المورد بالترتيب الصحيح في الـ nesting.
مثال للمبتدئ قبل ما ندخل في الشرح العلمي
تخيّل إنك دخلت غرفة فيها مكيف. لازم تطفي المكيف لما تخرج، حتى لو خرجت بسرعة أو حصل قطع كهربا فجأة. لو نسيت تطفيه، فاتورة الكهربا هتطلع مضاعفة. with في Python بيلعب دور حارس على باب الغرفة: ساعة ما تدخل الكتلة، الباب بيفتح ويشغّل المكيف. ساعة ما تخرج بأي طريقة (طبيعي، break، return، أو exception)، الحارس بيطفي المكيف ويقفل الباب أوتوماتيكياً. مفيش طريقة تنسى.
التعريف العلمي
الـ Context Manager في Python هو أي object بيطبّق method اسمها __enter__ و method تانية __exit__، حسب Python Data Model و PEP 343. لما الـ interpreter يقابل with expression as var:، بيعمل التالي بالترتيب:
- بيقيّم
expressionويرجّع object (لازم يكون context manager). - بينادي
__enter__()ويحط نتيجتها فيvar. - بينفّذ كتلة الـ
with. - سواء الكتلة خلصت طبيعي أو طلعت بـ exception، بينادي
__exit__(exc_type, exc_val, exc_tb).
لو الـ __exit__ رجع True، الـ exception بيتبلع وما بيكمّلش. لو رجع False أو None (الافتراضي)، الـ exception بيكمّل طريقه طبيعي. ده الفرق المهم اللي كتير ناس بتنساه.
مثال كود حقيقي: psycopg2 و PostgreSQL
قارن بين الطريقتين على connection حقيقي:
# الطريقة القديمة - 10 سطور وفيها فخ
import psycopg2
conn = psycopg2.connect("dbname=app user=postgres")
try:
cur = conn.cursor()
try:
cur.execute("SELECT id, name FROM users WHERE active = true")
rows = cur.fetchall()
finally:
cur.close()
finally:
conn.close()
# الطريقة الصحيحة - 4 سطور
import psycopg2
with psycopg2.connect("dbname=app user=postgres") as conn:
with conn.cursor() as cur:
cur.execute("SELECT id, name FROM users WHERE active = true")
rows = cur.fetchall()