python context manager, with statement, __enter__, __exit__, contextmanager, ExitStack
정의
Context manager는 __enter__와 __exit__ 메서드를 가진 객체로, with 문을 통해 자원 획득/해제를 안전하게 묶는다. 파일·락·DB 트랜잭션·임시 설정처럼 “들어가면 반드시 나와야 하는” 작업의 표준 패턴.
with 기본
with open("file.txt") as f: data = f.read()# f.close() 자동 호출 (예외 발생해도)with open("x.txt") as r, open("y.txt", "w") as w: w.write(r.read())
with 블록은 다음과 거의 동등:
mgr = open("file.txt")f = mgr.__enter__()try: data = f.read()except BaseException as e: if not mgr.__exit__(type(e), e, e.__traceback__): raiseelse: mgr.__exit__(None, None, None)
yield 전 = __enter__, yield 후 = __exit__. try/finally로 정리 보장.
yield value처럼 값을 넘기면 with ... as v로 받을 수 있다.
@contextmanagerdef temp_dir(): import tempfile, shutil, os path = tempfile.mkdtemp() try: yield path finally: shutil.rmtree(path)with temp_dir() as d: print(d) # /tmp/...
표준 라이브러리에서 자주 보는 context manager
자원
매니저
파일
open(path)
락
threading.Lock(), multiprocessing.Lock()
DB 커넥션
sqlite3.connect() (with 시 트랜잭션)
HTTP 세션
requests.Session(), httpx.Client()
잠시 stdout 가리기
contextlib.redirect_stdout(buf)
예외 묵살
contextlib.suppress(KeyError)
임시 디렉터리
tempfile.TemporaryDirectory()
변경 가능한 상태 잠시 변경
unittest.mock.patch(...)
from contextlib import suppresswith suppress(FileNotFoundError): os.remove("temp")
ExitStack: 동적 컨텍스트 합치기
여러 자원을 동적 개수로 묶을 때 (체이닝 with 문법은 컴파일 시점 고정).
from contextlib import ExitStackpaths = ["a.txt", "b.txt", "c.txt"]with ExitStack() as stack: files = [stack.enter_context(open(p)) for p in paths] process(files)# 모든 파일 자동 close (역순으로)
콜백도 등록 가능:
with ExitStack() as stack: stack.callback(print, "cleanup 1") stack.callback(print, "cleanup 2") work()# 등록 역순으로 cleanup 2, cleanup 1 실행
async with (async context manager)
비동기 자원 관리. __aenter__, __aexit__.
import aiohttpasync def fetch_all(urls): async with aiohttp.ClientSession() as session: for url in urls: async with session.get(url) as resp: yield await resp.text()
class IgnoreKeyError: def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): if exc_type is KeyError: print(f"ignoring {exc_value}") return True # 예외 삼킴 return False # 다른 예외는 전파with IgnoreKeyError(): d = {} d["missing"] # 삼켜짐
데코레이터로 쓰기
contextlib.ContextDecorator 상속 또는 @contextmanager 함수는 자체가 데코레이터로 작동.
@contextmanagerdef trace(name): print(f"enter {name}") yield print(f"exit {name}")@trace("work")def work(): print("working")work()# enter work# working# exit work
자주 만드는 패턴
임시 환경변수
import osfrom contextlib import contextmanager@contextmanagerdef env(**kwargs): old = {k: os.environ.get(k) for k in kwargs} os.environ.update(kwargs) try: yield finally: for k, v in old.items(): if v is None: os.environ.pop(k, None) else: os.environ[k] = vwith env(API_KEY="test-key"): run_test()
💬 댓글