contextlib — with 문 컨텍스트 유틸리티

contextlib — with 문 컨텍스트 유틸리티

contextlib 모듈은 with 문과 관련된 흔한 작업을 위한 유틸리티를 제공해요. 컨텍스트 매니저 타입과 with 문 컨텍스트 매니저에 대한 자세한 내용도 함께 참고하세요.

출처: Python 표준 라이브러리

본문

유틸리티

class contextlib.AbstractContextManager__enter__()__exit__()을 구현하는 클래스의 추상 기본 클래스예요. __enter__()의 기본 구현은 self를 반환하고, __exit__()은 기본적으로 None을 반환하는 추상 메서드입니다.

class contextlib.AbstractAsyncContextManager__aenter__()__aexit__()을 구현하는 클래스의 추상 기본 클래스예요. __aenter__()의 기본 구현은 self를 반환하고, __aexit__()은 기본적으로 None을 반환하는 추상 메서드입니다.

@contextlib.contextmanager — 클래스나 별도의 __enter__()/__exit__() 메서드를 만들지 않고도 with 문 컨텍스트 매니저의 팩토리 함수를 정의할 수 있게 해 주는 데코레이터예요.

많은 객체는 with 문에서 네이티브로 쓰이지만, 때로는 그 자체로 컨텍스트 매니저가 아니고 contextlib.closing과 함께 쓸 close() 메서드도 구현하지 않은 리소스를 관리해야 할 수 있어요. 정확한 리소스 관리를 보장하는 추상 예는 다음과 같습니다.

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

이 함수는 이렇게 쓸 수 있어요.

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

데코레이트된 함수는 호출 시 제너레이터-이터레이터를 반환해야 해요. 이 이터레이터는 정확히 하나의 값을 내놓아야 하며, 그 값이 with 문의 as 절 대상(있을 경우)에 바인딩됩니다.

제너레이터가 yield하는 지점에서 with 문에 중첩된 블록이 실행돼요. 블록이 종료된 뒤 제너레이터는 다시 재개됩니다. 블록에서 처리되지 않은 예외가 발생하면 yield가 일어난 지점에서 제너레이터 안에서 다시 발생합니다. 따라서 오류(있는 경우)를 잡거나 정리를 보장하기 위해 try…except…finally 문을 쓸 수 있어요. 예외를 완전히 억제하지 않고 로그만 남기거나 어떤 동작을 수행하기 위해 잡는 거라면, 제너레이터는 그 예외를 다시 발생시켜야 해요. 그렇지 않으면 컨텍스트 매니저가 with 문에 예외가 처리됐다고 알리고, with 문 바로 다음 문장부터 실행이 재개됩니다.

@contextmanagerContextDecorator를 사용하므로 만든 컨텍스트 매니저를 with 문뿐 아니라 데코레이터로도 쓸 수 있어요. 데코레이터로 쓰면 함수 호출마다 암시적으로 새 제너레이터 인스턴스가 만들어집니다(이렇게 해서 @contextmanager가 만드는 "일회용" 컨텍스트 매니저가 데코레이터로 쓰이기 위해 필요한 다중 호출 지원 요구를 충족합니다).

@contextlib.asynccontextmanager@contextlib.contextmanager와 비슷하지만 비동기 컨텍스트 매니저를 만들어요. 별도의 클래스나 __aenter__()/__aexit__() 메서드 없이 async with 문의 비동기 컨텍스트 매니저 팩토리 함수를 정의하는 데코레이터예요. 비동기 제너레이터 함수에 적용해야 합니다.

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
    conn = await acquire_db_connection()
    try:
        yield conn
    finally:
        await release_db_connection(conn)

async def get_all_users():
    async with get_connection() as conn:
        return conn.query('SELECT ...')

@asynccontextmanager로 정의된 컨텍스트 매니저는 데코레이터나 async with 문으로 쓸 수 있어요.

import time
from contextlib import asynccontextmanager

@asynccontextmanager
async def timeit():
    now = time.monotonic()
    try:
        yield
    finally:
        print(f'it took {time.monotonic() - now}s to run')

@timeit()
async def main():
    # ... async code ...

contextlib.closing(thing) — 블록 완료 시 thing을 닫는 컨텍스트 매니저를 반환해요. 기본적으로 다음과 같습니다.

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

덕분에 이렇게 쓸 수 있어요.

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)

page를 명시적으로 닫지 않아도 돼요. 오류가 발생해도 with 블록을 나올 때 page.close()가 호출됩니다.

참고: 리소스를 관리하는 대부분의 타입은 컨텍스트 매니저 프로토콜을 지원해서 with 문을 나올 때 thing을 닫아요. 따라서 closing()은 컨텍스트 매니저를 지원하지 않는 서드파티 타입에 가장 유용합니다. 이 예는 설명용으로만 쓴 것이라, urlopen()은 보통 컨텍스트 매니저에서 사용됩니다.

contextlib.aclosing(thing) — 블록 완료 시 thingaclose() 메서드를 호출하는 비동기 컨텍스트 매니저를 반환해요. 기본적으로 다음과 같습니다.

from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing(thing):
    try:
        yield thing
    finally:
        await thing.aclose()

중요하게도 aclosing()은 비동기 제너레이터가 break나 예외로 일찍 종료될 때 결정적으로(cleaning) 정리하는 것을 지원해요.

from contextlib import aclosing

async with aclosing(my_generator()) as values:
    async for value in values:
        if value == 42:
            break

이 패턴은 제너레이터의 비동기 종료 코드가 그 이터레이션과 같은 컨텍스트에서 실행되도록 보장해요(예외와 컨텍스트 변수가 예상대로 동작하고, 종료 코드가 의존하는 어떤 태스크의 수명 이후에 실행되지 않도록).

contextlib.nullcontext(enter_result=None)__enter__()에서 enter_result를 반환하지만 그 외에는 아무것도 하지 않는 컨텍스트 매니저를 반환해요. 선택적 컨텍스트 매니저의 대역용으로 쓰입니다.

def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
        # Use suppress to ignore all exceptions.
        cm = contextlib.suppress(Exception)
    else:
        # Do not ignore any exceptions, cm has no effect.
        cm = contextlib.nullcontext()
    with cm:
        # Do something

enter_result를 쓰는 예:

def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # If string, open file
        cm = open(file_or_path)
    else:
        # Caller is responsible for closing file
        cm = nullcontext(file_or_path)

    with cm as file:
        # Perform processing on the file

비동기 컨텍스트 매니저의 대역으로도 쓸 수 있어요.

async def send_http(session=None):
    if not session:
        # If no http session, create it with aiohttp
        cm = aiohttp.ClientSession()
    else:
        # Caller is responsible for closing the session
        cm = nullcontext(session)

    async with cm as session:
        # Send http requests with session

contextlib.suppress(*exceptions) — with 문 본문에서 지정한 예외가 발생하면 억제하고, with 문 끝 다음의 첫 문장부터 실행을 재개하는 컨텍스트 매니저를 반환해요.

예외를 완전히 억제하는 다른 모든 메커니즘과 마찬가지로, 이 컨텍스트 매니저는 프로그램 실행을 조용히 계속하는 것이 올바른 것으로 알려진 매우 특정한 오류를 다룰 때만 써야 해요.

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

with suppress(FileNotFoundError):
    os.remove('someotherfile.tmp')

이 코드는 다음과 같습니다.

try:
    os.remove('somefile.tmp')
except FileNotFoundError:
    pass

try:
    os.remove('someotherfile.tmp')
except FileNotFoundError:
    pass

이 컨텍스트 매니저는 재진입 가능(reentrant) 해요. with 블록의 코드가 BaseExceptionGroup을 일으키면 억제된 예외는 그룹에서 제거됩니다. 억제되지 않은 그룹의 예외는 원래 그룹의 derive() 메서드로 만든 새 그룹에서 다시 발생해요.

contextlib.redirect_stdout(new_target)sys.stdout을 임시로 다른 파일이나 파일류 객체로 리다이렉트하는 컨텍스트 매니저예요. 출력이 stdout에 하드와이어된 기존 함수·클래스에 유연성을 더해 줍니다.

예를 들어 help()의 출력은 보통 sys.stdout으로 보내져요. 출력을 io.StringIO 객체로 리다이렉트해 문자열로 캡처할 수 있습니다. 대체 스트림은 __enter__() 메서드에서 반환되므로 with 문의 대상으로 사용할 수 있어요.

with redirect_stdout(io.StringIO()) as f:
    help(pow)
s = f.getvalue()

help() 출력을 디스크 파일로 보내려면 출력을 일반 파일로 리다이렉트하세요.

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

help() 출력을 sys.stderr로 보내려면:

with redirect_stdout(sys.stderr):
    help(pow)

sys.stdout에 대한 전역 부작용 때문에 이 컨텍스트 매니저는 라이브러리 코드와 대부분의 스레드 애플리케이션에는 적합하지 않아요. 서브프로세스의 출력에는 영향이 없습니다. 그래도 많은 유틸리티 스크립트에는 유용한 방식이에요. 재진입 가능합니다.

contextlib.redirect_stderr(new_target)redirect_stdout()과 비슷하지만 sys.stderr을 다른 파일이나 파일류 객체로 리다이렉트합니다. 재진입 가능해요.

contextlib.chdir(path) — 현재 작업 디렉터리를 바꾸는 병렬 안전하지 않은 컨텍스트 매니저예요. 작업 디렉터리라는 전역 상태를 바꾸므로 대부분의 스레드·async 컨텍스트에는 적합하지 않아요. 제너레이터처럼 프로그램 실행이 일시적으로 양보되는 대부분의 비선형 코드 실행에도 적합하지 않아요. 원하지 않으면 이 컨텍스트 매니저가 활성화된 동안 yield하면 안 됩니다. chdir()의 간단한 래퍼로, 진입 시 현재 작업 디렉터리를 바꾸고 종료 시 이전 것을 복원해요. 재진입 가능합니다.

class contextlib.ContextDecorator — 컨텍스트 매니저를 데코레이터로도 사용할 수 있게 해 주는 기본 클래스예요. ContextDecorator를 상속한 컨텍스트 매니저는 평소처럼 __enter__()__exit__()을 구현해야 해요. __exit__은 데코레이터로 쓸 때도 선택적 예외 처리를 유지합니다. @contextmanager가 이 기능을 자동 제공합니다.

from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

이 클래스는 이렇게 쓸 수 있어요.

>>> @mycontext()
... def function():
...     print('The bit in the middle')
...
>>> function()
Starting
The bit in the middle
Finishing

>>> with mycontext():
...     print('The bit in the middle')
...
Starting
The bit in the middle
Finishing

이 변화는 다음 형태의 어떤 구조에도 대한 문법 설탕입니다.

def f():
    with cm():
        # Do stuff

ContextDecorator를 쓰면 이렇게 쓸 수 있어요.

@cm()
def f():
    # Do stuff

cm이 함수의 일부 조각이 아니라 전체 함수에 적용된다는 게 분명해지고(들여쓰기 한 단계를 아끼는 것도 좋고요), 이미 기본 클래스가 있는 기존 컨텍스트 매니저는 ContextDecorator를 믹스인으로 사용해 확장할 수 있어요.

참고: 데코레이트된 함수는 여러 번 호출될 수 있어야 하므로, 기본 컨텍스트 매니저는 여러 with 문에서 사용을 지원해야 해요. 그렇지 않다면 함수 안에 명시적 with 문을 둔 원래 구성을 써야 합니다.

class contextlib.AsyncContextDecoratorContextDecorator와 비슷하지만 비동기 함수 전용이에요.

from asyncio import run
from contextlib import AsyncContextDecorator

class mycontext(AsyncContextDecorator):
    async def __aenter__(self):
        print('Starting')
        return self

    async def __aexit__(self, *exc):
        print('Finishing')
        return False

이 클래스는 이렇게 쓸 수 있어요.

>>> @mycontext()
... async def function():
...     print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

>>> async def function():
...    async with mycontext():
...         print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

class contextlib.ExitStack — 다른 컨텍스트 매니저들과 정리 함수, 특히 선택적이거나 입력 데이터에 의해 결정되는 것들을 프로그래밍 방식으로 쉽게 결합하도록 설계된 컨텍스트 매니저예요. 예를 들어 파일 집합 하나를 단일 with 문으로 쉽게 처리할 수 있습니다.

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # All opened files will automatically be closed at the end of
    # the with statement, even if attempts to open files later
    # in the list raise an exception

__enter__() 메서드는 ExitStack 인스턴스를 반환하며 추가 연산은 하지 않아요. 각 인스턴스는 등록된 콜백의 스택을 유지하는데, 인스턴스가 닫힐 때(with 문 끝에서 명시적이든 암시적이든) 역순으로 호출됩니다. 컨텍스트 스택 인스턴스가 가비지 컬렉트될 때 콜백이 암시적으로 호출되지 않는다는 점에 주의하세요.

이 스택 모델은 __init__ 메서드에서 리소스를 획득하는 컨텍스트 매니저(파일 객체 등)를 올바르게 처리하기 위해 사용됩니다. 등록된 콜백이 등록의 역순으로 호출되므로, 등록된 콜백 집합으로 여러 중첩 with 문을 사용한 것처럼 동작합니다. 이는 예외 처리에도 적용돼요 — 내부 콜백이 예외를 억제하거나 대체하면 외부 콜백은 그 갱신된 상태를 기반으로 한 인자를 받습니다.

이것은 종료 콜백 스택을 올바르게 풀어내는 세부 사항을 처리하는 비교적 저수준 API예요. 애플리케이션 특정 방식으로 종료 스택을 조작하는 고수준 컨텍스트 매니저의 적합한 기반을 제공합니다.

class contextlib.AsyncExitStackExitStack과 비슷한 비동기 컨텍스트 매니저로, 동기·비동기 컨텍스트 매니저를 모두 결합할 수 있고 정리 로직에 코루틴을 쓸 수 있어요. close() 메서드는 구현되지 않으니 aclose()를 써야 합니다.

async with AsyncExitStack() as stack:
    connections = [await stack.enter_async_context(get_connection())
        for i in range(5)]
    # All opened connections will automatically be released at the end of
    # the async with statement, even if attempts to open a connection
    # later in the list raise an exception.

예제와 레시피

가변 개수의 컨텍스트 매니저 지원

ExitStack의 주요 사용 사례는 단일 with 문에서 가변 개수의 컨텍스트 매니저와 다른 정리 연산을 지원하는 거예요. 그 가변성은 필요한 컨텍스트 매니저 개수가 사용자 입력에 의해 결정되거나(사용자가 지정한 파일 집합을 여는 것처럼), 일부 컨텍스트 매니저가 선택적이어서 올 수 있어요.

with ExitStack() as stack:
    for resource in resources:
        stack.enter_context(resource)
    if need_special_resource():
        special = acquire_special_resource()
        stack.callback(release_special_resource, special)
    # Perform operations that use the acquired resources

보시다시피 ExitStack은 컨텍스트 관리 프로토콜을 네이티브로 지원하지 않는 임의 리소스를 with 문으로 관리하는 것도 아주 쉽게 해 줍니다.

enter 메서드의 예외 잡기

__enter__() 구현의 예외를, with 문 본문이나 컨텍스트 매니저의 __exit__() 메서드의 예외를 실수로 잡지 않으면서 잡는 것이 가끔 바람직해요. ExitStack을 사용하면 컨텍스트 관리 프로토콜의 단계를 약간 분리해 이를 허용할 수 있습니다.

stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # handle __enter__ exception
else:
    with stack:
        # Handle normal case

실제로 이렇게 할 필요가 있다면 기반 API가 try/except/finally 문과 함께 쓸 직접 리소스 관리 인터페이스를 제공해야 한다는 신호일 가능성이 크지만, 모든 API가 그렇게 잘 설계되진 않아요. 컨텍스트 매니저가 유일한 리소스 관리 API로 제공될 때, ExitStack은 with 문에서 직접 처리할 수 없는 다양한 상황을 더 쉽게 다루게 해 줍니다.

enter 구현에서 정리하기

ExitStack.push() 문서에서 밝혔듯, 이 메서드는 __enter__() 구현의 이후 단계가 실패할 때 이미 할당된 리소스를 정리하는 데 유용할 수 있어요.

리소스 획득·해제 함수와 선택적 검증 함수를 받아 컨텍스트 관리 프로토콜에 매핑하는 컨텍스트 매니저에 대해 이렇게 하는 예입니다.

from contextlib import contextmanager, AbstractContextManager, ExitStack

class ResourceManager(AbstractContextManager):

    def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
        self.acquire_resource = acquire_resource
        self.release_resource = release_resource
        if check_resource_ok is None:
            def check_resource_ok(resource):
                return True
        self.check_resource_ok = check_resource_ok

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.push(self)
            yield
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            stack.pop_all()

    def __enter__(self):
        resource = self.acquire_resource()
        with self._cleanup_on_error():
            if not self.check_resource_ok(resource):
                msg = "Failed validation for {!r}"
                raise RuntimeError(msg.format(resource))
        return resource

    def __exit__(self, *exc_details):
        # We don't need to duplicate any of our resource release logic
        self.release_resource()

try-finally와 플래그 변수 대체

가끔 보이는 패턴은 finally 절의 본문을 실행할지 나타내는 플래그 변수를 가진 try-finally 문입니다. 가장 단순한 형태는 이렇게 생겼어요.

cleanup_needed = True
try:
    result = perform_operation()
    if result:
        cleanup_needed = False
finally:
    if cleanup_needed:
        cleanup_resources()

어떤 try 문 기반 코드든 그렇듯, 설정 코드와 정리 코드가 임의로 긴 코드 섹션으로 분리될 수 있어 개발·검토에 문제가 될 수 있어요.

ExitStack은 with 문 끝에서 실행할 콜백을 등록한 뒤 나중에 그 콜백 실행을 건너뛰기로 결정하는 것을 가능하게 합니다.

from contextlib import ExitStack

with ExitStack() as stack:
    stack.callback(cleanup_resources)
    result = perform_operation()
    if result:
        stack.pop_all()

이렇게 하면 별도의 플래그 변수 대신 의도한 정리 동작을 앞으로 명시할 수 있어요. 특정 애플리케이션이 이 패턴을 많이 쓴다면 작은 헬퍼 클래스로 더 단순화할 수 있습니다.

from contextlib import ExitStack

class Callback(ExitStack):
    def __init__(self, callback, /, *args, **kwds):
        super().__init__()
        self.callback(callback, *args, **kwds)

    def cancel(self):
        self.pop_all()

with Callback(cleanup_resources) as cb:
    result = perform_operation()
    if result:
        cb.cancel()

리소스 정리가 이미 독립 함수로 깔끔하게 묶여 있지 않다면, ExitStack.callback()의 데코레이터 형태를 사용해 리소스 정리를 미리 선언할 수도 있어요.

from contextlib import ExitStack

with ExitStack() as stack:
    @stack.callback
    def cleanup_resources():
        ...
    result = perform_operation()
    if result:
        stack.pop_all()

데코레이터 프로토콜 동작 방식 때문에 이렇게 선언한 콜백 함수는 매개변수를 받을 수 없어요. 대신 해제할 리소스는 클로저 변수로 접근해야 합니다.

컨텍스트 매니저를 함수 데코레이터로 사용

ContextDecorator는 컨텍스트 매니저를 일반 with 문과 함수 데코레이터 둘 다로 사용할 수 있게 해 줍니다. 예를 들어 진입·종료 시간을 추적하는 로거로 함수나 문 그룹을 감싸는 것이 유용할 때가 있어요. 함수 데코레이터와 컨텍스트 매니저를 따로 쓰지 않고 ContextDecorator를 상속하면 두 능력을 단일 정의로 얻을 수 있습니다.

from contextlib import ContextDecorator
import logging

logging.basicConfig(level=logging.INFO)

class track_entry_and_exit(ContextDecorator):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        logging.info('Entering: %s', self.name)

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Exiting: %s', self.name)

이 클래스의 인스턴스는 컨텍스트 매니저로도 쓸 수 있고:

with track_entry_and_exit('widget loader'):
    print('Some time consuming activity goes here')
    load_widget()

함수 데코레이터로도 쓸 수 있어요.

@track_entry_and_exit('widget loader')
def activity():
    print('Some time consuming activity goes here')
    load_widget()

컨텍스트 매니저를 함수 데코레이터로 쓸 때 한 가지 추가 제한이 있음을 기억하세요: __enter__()의 반환값에 접근할 방법이 없어요. 그 값이 필요하면 여전히 명시적 with 문을 써야 합니다.

일회용, 재사용, 재진입 컨텍스트 매니저

대부분의 컨텍스트 매니저는 with 문에서 한 번만 효과적으로 사용할 수 있게 작성돼요. 이런 일회용 컨텍스트 매니저는 사용할 때마다 새로 만들어야 해요 — 두 번째 사용을 시도하면 예외가 발생하거나 제대로 동작하지 않습니다. 이런 흔한 제한 때문에 일반적으로 컨텍스트 매니저를 사용하는 with 문의 헤더에서 직접 만드는 것이 좋습니다(위의 모든 사용 예에서 본 것처럼).

파일은 사실상 일회용 컨텍스트 매니저의 예인데, 첫 with 문이 파일을 닫아 그 파일 객체로는 더 이상 I/O 연산이 불가능하기 때문이에요. @contextmanager로 만든 컨텍스트 매니저도 일회용이며, 두 번째 사용을 시도하면 기본 제너레이터가 yield하지 못한다고 불평합니다.

>>> from contextlib import contextmanager
>>> @contextmanager
... def singleuse():
...     print("Before")
...     yield
...     print("After")
...
>>> cm = singleuse()
>>> with cm:
...     pass
...
Before
After
>>> with cm:
...     pass
...
Traceback (most recent call last):
    ...
RuntimeError: generator didn't yield

재진입 컨텍스트 매니저

더 정교한 컨텍스트 매니저는 "재진입 가능(reentrant)"할 수 있어요. 이 컨텍스트 매니저는 여러 with 문에서 쓸 수 있을 뿐 아니라, 이미 같은 컨텍스트 매니저를 사용 중인 with 문 안에서도 쓸 수 있습니다.

threading.RLock은 재진입 컨텍스트 매니저의 예이고, suppress(), redirect_stdout(), chdir()도 그래요. 재진입 사용의 매우 간단한 예는 다음과 같습니다.

>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> stream = StringIO()
>>> write_to_stream = redirect_stdout(stream)
>>> with write_to_stream:
...     print("This is written to the stream rather than stdout")
...     with write_to_stream:
...         print("This is also written to the stream")
...
>>> print("This is written directly to stdout")
This is written directly to stdout
>>> print(stream.getvalue())
This is written to the stream rather than stdout
This is also written to the stream

실제 세계의 재진입 예는 여러 함수가 서로를 호출하는 경우가 많아 이 예보다 훨씬 복잡할 가능성이 커요. 또 재진입 가능이 스레드 안전과 같은 뜻은 아니라는 점에 유의하세요. 예를 들어 redirect_stdout()sys.stdout을 다른 스트림에 바인딩해 시스템 상태를 전역으로 수정하므로 확실히 스레드 안전하지 않아요.

재사용 컨텍스트 매니저

일회용·재진입과 구별되는 "재사용 가능(reusable)" 컨텍스트 매니저가 있어요 (정확히는 "재사용 가능하지만 재진입은 아닌" 컨텍스트 매니저 — 재진입 컨텍스트 매니저도 재사용 가능하니까요). 이 컨텍스트 매니저는 여러 번 사용을 지원하지만, 특정 컨텍스트 매니저 인스턴스가 이미 포함하는 with 문에서 사용된 적이 있으면 실패(또는 제대로 동작하지 않음)합니다.

threading.Lock은 재사용 가능하지만 재진입은 아닌 컨텍스트 매니저의 예입니다(재진입 락이 필요하면 threading.RLock을 써야 해요). ExitStack도 재사용 가능하지만 재진입은 아닌 컨텍스트 매니저의 또 다른 예인데, 어디서 콜백이 추가됐든 어떤 with 문을 나올 때 등록된 모든 콜백을 호출하기 때문이에요.

>>> from contextlib import ExitStack
>>> stack = ExitStack()
>>> with stack:
...     stack.callback(print, "Callback: from first context")
...     print("Leaving first context")
...
Leaving first context
Callback: from first context
>>> with stack:
...     stack.callback(print, "Callback: from second context")
...     print("Leaving second context")
...
Leaving second context
Callback: from second context
>>> with stack:
...     stack.callback(print, "Callback: from outer context")
...     with stack:
...         stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Callback: from outer context
Leaving outer context

예제 출력에서 보듯 단일 스택 객체를 여러 with 문에서 재사용하면 올바르게 동작하지만, 중첩하려 하면 가장 안쪽 with 문 끝에서 스택이 비워져 바람직하지 않은 동작이 나올 수 있어요.

단일 인스턴스를 재사용하는 대신 별도의 ExitStack 인스턴스를 쓰면 그 문제를 피할 수 있습니다.

>>> from contextlib import ExitStack
>>> with ExitStack() as outer_stack:
...     outer_stack.callback(print, "Callback: from outer context")
...     with ExitStack() as inner_stack:
...         inner_stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Leaving outer context
Callback: from outer context

더 알아보기