آنچه در این مقاله میخوانید [پنهانسازی]
Context Manager در پایتون روشی است برای اجرای قطعی کدهای راهاندازی و جمعبندی با دستور with؛ یعنی قبل از ورود به بلاک با __enter__ آمادهسازی انجام میشود و در پایان بلاک (حتی در صورت خطا) __exit__ فراخوانی و منبع آزاد میشود. میتوانید آن را با نوشتن کلاس دارای __enter__/__exit__ پیاده کنید یا با contextlib سریعتر بسازید.
Context Manager دقیقا چه مسئلهای را حل میکند؟
پاسخ کوتاه: مدیریت عمر منابع به شکل مطمئن. فایل، قفل، اتصال شبکه یا تراکنش باید حتما آزاد شوند؛ با with فراموش نمیکنید. حتی اگر استثنا رخ دهد، پاکسازی در __exit__ یا بلوک finally اتفاق میافتد.
دستور with چگونه کار میکند؟
- ابژه سمت راست with باید Context Manager باشد.
- پایتون __enter__ را صدا میزند و نتیجهاش را به as متغیر میدهد (اگر as داشته باشید).
- بدنه with اجرا میشود.
- در پایان، چه خطا رخ دهد چه نه، __exit__(exc_type, exc, tb) اجرا میشود.
# نمونه ساده فایل
with open("data.txt", "w", encoding="utf-8") as f:
f.write("hello")
# اینجا فایل قطعا بسته شده استنوشتن Context Manager با کلاس: __enter__ و __exit__
وقتی نیاز خاص دارید، یک کلاس با این دو متد بنویسید. __enter__ باید ابژهای را که میخواهید با as دریافت شود برگرداند؛ __exit__ هم همیشه فراخوانی میشود و میتواند خطا را مدیریت کند.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # در with ... as t: این شیء به t میرسد
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.start
print(f"done in {self.elapsed:.3f}s")
# با False (یا None) اجازه میدهیم استثناها عبور کنند
return False
# استفاده
with Timer() as t:
sum(i for i in range(1_000_000))
# در پایان زمان اجرا چاپ میشوداگر __exit__ مقدار True برگرداند، استثنای رخداده در بدنه with سرکوب میشود. این کار فقط وقتی موجه است که واقعا میخواهید خطا نادیده گرفته شود؛ در غیر این صورت، بهتر است False برگردانید تا اشکالزدایی سخت نشود.
ساخت سریع با contextlib.contextmanager
اگر فقط میخواهید قبل/بعد یک قطعه کد کاری انجام دهید، دکوریتور contextmanager نوشتن کلاس را بینیاز میکند. قبل از yield آمادهسازی، بعد از yield پاکسازی است. حتما پاکسازی را در finally بگذارید.
import sqlite3
from contextlib import contextmanager
@contextmanager
def open_db(path):
conn = sqlite3.connect(path)
try:
yield conn # بدنه with از این اتصال استفاده میکند
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# استفاده
with open_db("example.db") as db:
db.execute("create table if not exists t(x)")
db.execute("insert into t(x) values (?)", ("hi",))ابزارهای کاربردی contextlib
- suppress: استثناهای مشخصی را نادیده بگیر.
from contextlib import suppress with suppress(FileNotFoundError): print(open("maybe.txt").read()) # اگر نبود، خطا نمیدهد - closing: برای ابژههایی که فقط متد close دارند و Context Manager نیستند.
from contextlib import closing class Resource: def close(self): print("closed") with closing(Resource()): # استفاده از منبع pass # در پایان close صدا زده میشود - nullcontext: وقتی تنظیم اختیاری دارید؛ اگر منبعی ندارید، با nullcontext همان API حفظ میشود.
from contextlib import nullcontext debug = False cm = open("log.txt", "a", encoding="utf-8") if debug else nullcontext() with cm as f: if debug: f.write("started\n") # ادامه کار - ExitStack: مدیریت پویا و ترکیب چند Context Manager که از قبل تعدادشان معلوم نیست.
from contextlib import ExitStack filenames = ["a.txt", "b.txt", "c.txt"] with ExitStack() as stack: files = [stack.enter_context(open(name, "w", encoding="utf-8")) for name in filenames] for i, f in enumerate(files, 1): f.write(f"file {i}\n") # همه فایلها خودکار بسته شدند
مدیریت چند منبع در یک with یا با ExitStack
وقتی تعداد ثابت است، میتوانید چند Context Manager را در یک with بیاورید. ترتیب باز شدن از چپ به راست است و بستن به ترتیب معکوس.
with open("in.txt", encoding="utf-8") as src, open("out.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())وقتی تعداد منابع در زمان اجرا معلوم میشود، از ExitStack استفاده کنید (نمونه بالا).
Context Manager ناهمگام: async with و __aenter__/__aexit__
وقتی آمادهسازی و پاکسازی نیازمند عملیات ناهمگام است، از async with استفاده میشود و شیء باید __aenter__ و __aexit__ ناهمگام داشته باشد. یک نمونه رایج، کار با TaskGroup در asyncio است.
import asyncio
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(asyncio.sleep(1))
tg.create_task(asyncio.sleep(2))
# در اینجا همه تسکها تمام یا مدیریت خطا شدهاند
asyncio.run(main())برای یادگیری عمیقتر این الگو در برنامههای همزمان، آموزش مدیریت همزمان چند وظیفه با asyncio TaskGroup در پایتون مسیر خوبی است.
الگوی استفاده در برنامههای CLI
در ابزارهای خط فرمان، Context Managerها برای باز و بستن فایلهای پیکربندی، قفلگذاری یا مدیریت لاگ بسیار مفیدند. اگر در حال ساخت CLI هستید، آشنایی با Typer کنار این الگوها کمک میکند کد تمیزتری بنویسید. نگاه کنید به آموزش Typer در پایتون: ساخت ابزار خط فرمان حرفهای.
خطاهای رایج و نکات ایمنی
- سرکوب ناخواسته استثنا: برگرداندن True از __exit__ خطا را قورت میدهد. مگر با دلیل روشن، False برگردانید. برای نادیده گرفتن هدفمند، از contextlib.suppress استفاده کنید.
- فراموشی finally در generator-based: در contextmanager فقط یک بار yield مجاز است. پاکسازی را حتما در finally بگذارید تا حتی در صورت خطا اجرا شود.
- اتکا به __del__ برای پاکسازی: زمان فراخوانی __del__ قطعی نیست. پاکسازی منابع را به Context Manager یا بلوک finally بسپارید.
- ایجاد منبع در __enter__ بدون برنامه پشتیبان: اگر وسط __enter__ خطا رخ دهد، __exit__ اجرا نمیشود. منابع موقتی را یا قبل از __enter__/after yield مدیریت کنید یا در همان __enter__ با try/except خودتان جمع کنید.
- استفاده بعد از خروج: پس از خروج از with، منبع ممکن است بسته/آزاد شده باشد. استفاده بعدی میتواند خطا یا رفتار نامشخص ایجاد کند.
- ترتیب بستهشدن در چند Context Manager: بستن از راست به چپ است. اگر وابستگی زمانی دارید، ترتیب را با دقت بچینید یا از ExitStack کمک بگیرید.
- بالا انداختن خطای جدید در __exit__: اگر هنگام لاگکردن/پاکسازی خطای دیگری بالا بیاید، خطای اصلی پنهان میشود. استثناهای ثانویه را مدیریت کنید.
روش بررسی صحت و اشکالزدایی
- اطمینان از بستهشدن فایل:
f = open("tmp.txt", "w", encoding="utf-8") with f: f.write("ok") print(f.closed) # True - بازبینی مسیر اجرای خطا: با ایجاد خطا در بدنه with بررسی کنید __exit__ یا بلاک finally اجرا میشود.
class CM: def __enter__(self): print("enter") return self def __exit__(self, exc_type, exc, tb): print("exit", exc_type.__name__ if exc_type else None) return False # نگذارید خطا مخفی شود try: with CM(): 1 / 0 except ZeroDivisionError: print("caught!") - تست ترکیب پویا: برای چند منبع پویا از ExitStack استفاده کنید و با شمارش/پرینت اطمینان بگیرید همه آزاد میشوند.
چه زمانی کدام روش مناسبتر است؟
- کلاس با __enter__/__exit__: وقتی حالت داخلی دارید یا میخواهید ابژهای با عمر قابل مدیریت در اختیار کاربر بگذارید.
- @contextmanager: برای پوشاندن یک API موجود با before/after سریع و بدون نیاز به نگهداشتن state پیچیده.
- ExitStack / AsyncExitStack: وقتی تعداد/نوع contextها در زمان اجرا معلوم میشود یا نیاز به ترکیب انعطافپذیر دارید.
- async with: هر جا آمادهسازی/پاکسازی ناهمگام است (سوکتهای async، کلاینتهای HTTP async، TaskGroup).
گام بعدی چیست؟
از امروز هر منبعی که باید قطعا آزاد شود را در with قرار دهید. اگر الگوی خاصی تکرار میشود، آن را به شکل یک Context Manager کوچک (با کلاس یا contextlib) تبدیل کنید. در پروژههای ناهمگام، حتما الگوی async with و مدیریت گروهی وظایف را تمرین کنید و برای تکمیل مسیر، سر بزنید به آموزش مدیریت همزمان چند وظیفه با asyncio TaskGroup در پایتون.







