[Final] PEP 343 - The “with” Statement

원문 링크: PEP 343 - The “with” Statement

상태: Final 유형: Standards Track 작성일: 13-May-2005

PEP 343 – with

초록 (Abstract)

이 PEP(Python Enhancement Proposal)는 Python 언어에 새로운 with 문을 추가하여, try/finally 문의 표준적인 사용 패턴을 간결하게 만들 수 있도록 합니다. with 문 내에서, 컨텍스트 매니저(context manager)with 문 본문으로 진입하고 종료할 때 호출되는 __enter__()__exit__() 메서드를 제공합니다.

작성자 주 (Author’s Note)

이 PEP는 원래 Guido van Rossum에 의해 작성되었으며, 이후 python-dev에서 논의된 내용을 반영하여 Alyssa (Nick) Coghlan이 업데이트했습니다. Python 2.5 알파 릴리스 주기 동안 이 PEP의 용어와 관련 문서 및 구현에서 문제점이 발견되었고, 첫 번째 Python 2.5 베타 릴리스 시점에 PEP가 안정화되었습니다.

서론 (Introduction)

PEP 340 및 대안에 대한 많은 논의 끝에, Guido는 PEP 340을 철회하고 PEP 310의 약간 수정된 버전을 제안했습니다. 추가 논의 후, 일시 중지된 제너레이터(generator)에서 예외를 발생시키기 위한 throw() 메서드와 새로운 GeneratorExit 예외를 발생시키는 close() 메서드가 다시 추가되었습니다. 이 변경 사항들은 python-dev에서 처음 제안되어 만장일치로 승인되었습니다. 또한, 키워드가 with로 변경되었습니다.

이 PEP가 채택된 후, 다음과 같은 PEP들은 중복으로 인해 기각되었습니다.

  • PEP 310, Reliable Acquisition/Release Pairs: with 문에 대한 원래 제안이었습니다.
  • PEP 319, Python Synchronize/Asynchronize Block: 이 PEP의 사용 사례는 적절한 with 문 컨트롤러를 제공함으로써 처리될 수 있습니다.

PEP 340 및 PEP 346도 이 PEP와 중복되었으나, 이 PEP가 제출되면서 자발적으로 철회되었습니다.

동기 및 요약 (Motivation and Summary)

PEP 340(Anonymous Block Statements)은 제너레이터를 블록 템플릿으로 사용하고, 예외 처리 및 마무리(finalization) 기능을 제너레이터에 추가하는 등 여러 강력한 아이디어를 결합했습니다. 그러나 이는 내부적으로 잠재적인 반복(looping) 구조라는 점 때문에 많은 반대에 부딪혔습니다. 이는 breakcontinue가 블록 문 내에서 사용될 경우, 자원 관리 도구로 사용되더라도 블록 문을 깨거나 계속 진행시킬 수 있음을 의미했습니다.

결정적인 계기는 Raymond Chen의 흐름 제어 매크로에 대한 비판을 읽은 후였습니다. Raymond는 매크로에 흐름 제어를 숨기는 것이 코드를 이해하기 어렵게 만든다고 주장했으며, Guido는 이 주장이 C뿐만 아니라 Python에도 적용된다고 생각했습니다. PEP 340 템플릿이 모든 종류의 제어 흐름을 숨길 수 있음을 깨달은 것입니다.

반면, PEP 310의 with 문은 흐름 제어를 숨기지 않습니다. finally 스위트(suite)가 일시적으로 흐름을 중단시키더라도, 결국에는 finally 스위트가 없었던 것처럼 흐름이 재개됩니다.

PEP 310은 대략 다음과 같은 구문을 제안했습니다. ( VAR = 부분은 선택 사항입니다.)

with VAR = EXPR:
    BLOCK

이는 대략 다음과 같이 번역됩니다.

VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()

이러한 접근 방식은 BLOCK 내에서 예외가 발생하거나 break, continue, return과 같은 비지역적 goto가 실행될 경우, finally 절이 여전히 실행되도록 보장합니다.

이 아이디어는 Guido가 PEP 310을 지지하도록 이끌었지만, PEP 340의 제너레이터를 ‘템플릿’으로 사용하여 잠금 획득 및 해제 또는 파일 열기 및 닫기와 같은 추상화를 구현하는 강력한 아이디어를 포기할 수 없었습니다.

Phillip Eby의 PEP 340에 대한 반대 제안에 영감을 받아, 적절한 제너레이터를 필요한 __enter__()__exit__() 메서드를 가진 객체로 변환하는 데코레이터를 만들려고 시도했습니다. 여기서 문제에 부딪혔는데, 잠금(locking) 예시에서는 어렵지 않았지만, 파일 열기 예시에서는 불가능했습니다.

해결책은 VAR__enter__() 메서드 호출의 결과를 받도록 하고, EXPR의 값을 저장하여 나중에 __exit__() 메서드를 호출하는 방식으로 번역을 약간 변경하는 것이었습니다. 이렇게 하면 제너레이터를 컨텍스트 매니저로 변환하는 데코레이터를 쉽게 작성할 수 있게 됩니다.

이후 with VAR = EXPR: 구문이 VAREXPR의 값을 직접 받지 않는다는 점에서 오해의 소지가 있다는 논의가 있었습니다. PEP 340에서 아이디어를 빌려와 with EXPR as VAR: 구문이 채택되었습니다.

추가 논의를 통해, 제너레이터 내에서 예외를 ‘볼’ 수 있는 기능(throw() 메서드)이 중요하다고 판단되었습니다. 이는 with 문이 루프처럼 사용되는 것을 방지하면서도 예외 로깅 등을 가능하게 합니다. 또한, 제너레이터가 가비지 컬렉션될 때 자동으로 호출되는 close() 메서드도 제안되었으며, 이는 특별한 GeneratorExit 예외를 발생시킵니다.

이러한 변경 사항들을 통해 try-finally 문 내에서 yield 문을 허용할 수 있게 되었고, finally 절이 (결국) 실행될 것을 보장할 수 있게 되었습니다. (제너레이터에 대한 자세한 변경 사항은 PEP 342에서 다룹니다.)

사용 사례 (Use Cases)

자세한 내용은 마지막의 “예시” 섹션을 참조하십시오.

사양: with 문 (Specification: The ‘with’ Statement)

다음과 같은 구문의 새로운 문이 제안됩니다.

with EXPR as VAR:
    BLOCK

여기서 withas는 새로운 키워드입니다. EXPR은 임의의 표현식이며, VAR는 단일 할당 대상입니다. VAR는 쉼표로 구분된 변수 시퀀스가 될 수 없지만, 괄호로 묶인 쉼표로 구분된 변수 시퀀스는 가능합니다. (as VAR 부분은 선택 사항입니다.)

위 문의 번역은 다음과 같습니다.

mgr = (EXPR)
exit = type(mgr).__exit__ # 아직 호출하지 않음
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value # "as VAR"가 있는 경우에만
        BLOCK
    except: # 예외 발생 시 처리
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise # exit()가 True를 반환하면 예외는 삼켜짐
    finally: # 정상 및 비지역적 goto의 경우 처리
        if exc:
            exit(mgr, None, None, None)

여기서 소문자 변수(mgr, exit, value, exc)는 내부 변수이며 사용자에게는 접근할 수 없습니다. 이는 주로 특별한 레지스터 또는 스택 위치로 구현될 것입니다.

mgr.__exit__()의 호출 규칙은 다음과 같습니다.

  • BLOCK의 정상 완료 또는 비지역적 goto(break, continue, return)를 통해 finally 스위트에 도달한 경우, mgr.__exit__()는 세 개의 None 인자와 함께 호출됩니다.
  • BLOCK에서 발생한 예외를 통해 finally 스위트에 도달한 경우, mgr.__exit__()는 예외 유형, 값, 트레이스백을 나타내는 세 개의 인자와 함께 호출됩니다.

중요: mgr.__exit__()가 “참(true)” 값을 반환하면 예외는 “삼켜집니다(swallowed)”. 즉, with 문 내에서 예외가 발생했더라도 exit()True를 반환하면 with 문 다음 문에서 실행이 계속됩니다. 그러나 with 문이 비지역적 goto(break, continue, return)를 통해 종료된 경우, mgr.__exit__()의 반환 값과 관계없이 이 비지역적 반환이 재개됩니다. 이는 mgr.__exit__()가 예외를 삼킬 수 있도록 하면서도, 기본 반환 값인 None이 거짓(false)이므로 예외가 다시 발생하도록 하여 예외 삼키기를 너무 쉽게 만들지 않기 위함입니다.

__exit__()에 예외 세부 정보를 전달하는 동기는 트랜잭션(transactional()) 사용 사례(아래 예시 3)에서 비롯되었습니다. 이 예시의 템플릿은 예외 발생 여부에 따라 트랜잭션을 커밋하거나 롤백해야 합니다.

__exit__() 메서드는 전달된 오류를 다시 발생시키지 않아야 합니다. 오류를 다시 발생시키는 것은 항상 __exit__() 메서드를 호출하는 호출자의 책임입니다.

전환 계획 (Transition Plan)

Python 2.5에서는 from __future__ import with_statement 미래 문이 있을 경우에만 새로운 구문이 인식됩니다. 이 경우 withas가 키워드가 됩니다. 미래 문이 없으면 with 또는 as를 식별자로 사용하면 stderr에 경고가 발행됩니다. Python 2.6에서는 새로운 구문이 항상 인식되며, withas는 항상 키워드입니다.

제너레이터 데코레이터 (Generator Decorator)

PEP 342가 채택되면서, 정확히 한 번 yield하는 제너레이터를 사용하여 with 문을 제어하는 데코레이터를 작성할 수 있게 되었습니다. GeneratorContextManager 클래스를 사용하여 제너레이터 함수를 컨텍스트 매니저 팩토리로 변환하는 contextmanager 데코레이터를 구현할 수 있습니다.

강력한 구현은 표준 라이브러리의 일부가 될 것입니다.

# GeneratorContextManager 및 contextmanager 데코레이터의 스케치
class GeneratorContextManager(object):
    def __init__(self, gen):
        self.gen = gen
    def __enter__(self):
        try:
            return self.gen.next()
        except StopIteration:
            raise RuntimeError("generator didn't yield")
    def __exit__(self, type, value, traceback):
        if type is None:
            try:
                self.gen.next()
            except StopIteration:
                return
            else:
                raise RuntimeError("generator didn't stop")
        else:
            try:
                self.gen.throw(type, value, traceback)
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration:
                return True
            except:
                # throw() 프로토콜과 __exit__() 프로토콜 간의 불일치 해결
                # __exit__() 자체에 실패하지 않는 한 예외를 발생시키지 않아야 함
                pass # (원문에는 주석 처리된 raise 조건이 있으나, 스케치이므로 간략화)

def contextmanager(func):
    def helper(*args, **kwds):
        return GeneratorContextManager(func(*args, **kwds))
    return helper

이 데코레이터는 다음과 같이 사용될 수 있습니다.

@contextmanager
def opening(filename):
    f = open(filename)
    try:
        yield f
    finally:
        f.close()

표준 라이브러리의 컨텍스트 매니저 (Context Managers in the Standard Library)

파일, 소켓, 잠금(lock)과 같은 특정 객체에 __enter__()__exit__() 메서드를 부여하여 직접 with 문에서 사용할 수 있도록 할 수 있습니다. 예를 들어, with locking(myLock): BLOCK 대신 with myLock: BLOCK와 같이 작성할 수 있습니다.

그러나 이러한 방식은 f = open(filename); with f: BLOCK1; with f: BLOCK2와 같이 fBLOCK2에 진입하기 전에 닫히는 오해를 불러일으킬 수 있으므로 주의해야 합니다. 이러한 실수는 쉽게 진단할 수 있습니다.

Python 2.5에서는 다음 유형들이 컨텍스트 매니저로 식별되었습니다.

  • file
  • thread.LockType
  • threading.Lock
  • threading.RLock
  • threading.Condition
  • threading.Semaphore
  • threading.BoundedSemaphore

decimal 모듈에도 with 문 본문 내에서 지역 십진수 산술 컨텍스트를 지원하고, with 문을 종료할 때 원래 컨텍스트를 자동으로 복원하는 컨텍스트 매니저가 추가될 것입니다.

표준 용어 (Standard Terminology)

이 PEP는 __enter__()__exit__() 메서드로 구성된 프로토콜을 “컨텍스트 관리 프로토콜(context management protocol)“이라고 부르고, 이 프로토콜을 구현하는 객체를 “컨텍스트 매니저(context manager)“라고 부를 것을 제안합니다.

with 키워드 바로 뒤에 오는 표현식은 “컨텍스트 표현식(context expression)”이며, 이는 with 문 본문 기간 동안 컨텍스트 매니저가 설정하는 런타임 환경에 대한 주요 단서를 제공합니다.

컨텍스트 매니저 캐싱 (Caching Context Managers)

많은 컨텍스트 매니저(예: 파일 및 제너레이터 기반 컨텍스트)는 한 번만 사용되는 객체입니다. __exit__() 메서드가 호출되면 컨텍스트 매니저는 더 이상 사용 가능한 상태가 아닙니다.

with 문마다 새로운 매니저 객체를 요구하는 것이 멀티스레드 코드 및 중첩된 with 문에서 동일한 컨텍스트 매니저를 사용하려 할 때 발생하는 문제를 피하는 가장 쉬운 방법입니다. 재사용을 지원하는 모든 표준 라이브러리 컨텍스트 매니저가 threading 모듈에서 비롯되었다는 것은 우연이 아닙니다.

해결된 문제 (Resolved Issues)

다음 문제들은 BDFL(Benevolent Dictator For Life)의 승인과 python-dev에서의 주요 반대 의견 부족으로 해결되었습니다.

  • 제너레이터-이터레이터가 오작동할 때 GeneratorContextManager는 어떤 예외를 발생시켜야 하는가? Guido는 이 경우와 PEP 342의 제너레이터 close() 메서드 모두에 대해 RuntimeError를 선택했습니다. RuntimeError는 프로그래머가 코드를 수정하도록 유도하는 데 적합하며, 무한 재귀 감지 등 핵심 Python 코드에서 이미 사용되는 선례가 있습니다.
  • with 문에 관련된 클래스에 해당 메서드가 없을 경우 TypeError 대신 AttributeError를 발생시키는 것이 허용됩니다.
  • __enter__/__exit__ 메서드를 가진 객체는 “컨텍스트 매니저”라고 불리며, 제너레이터 함수를 컨텍스트 매니저 팩토리로 변환하는 데코레이터는 contextlib.contextmanager입니다.

거부된 옵션 (Rejected Options)

  • 예외 억제 금지: 초기에는 숨겨진 흐름 제어를 피하기 위해 예외 억제가 금지되었으나, 구현상의 어려움으로 인해 Guido는 예외 억제 기능을 복원했습니다.
  • __context__() 메서드: 이터러블의 __iter__() 메서드와 유사한 __context__() 메서드를 추가하자는 제안이 있었으나, 설명 및 작동 방식에 대한 지속적인 문제로 인해 Guido는 이 개념을 완전히 제거했습니다.
  • PEP 342 제너레이터 API 직접 사용: with 문 정의에 PEP 342의 향상된 제너레이터 API를 직접 사용하자는 아이디어도 잠시 고려되었으나, 제너레이터 기반이 아닌 컨텍스트 매니저 작성을 너무 어렵게 만든다는 이유로 빠르게 기각되었습니다.

예시 (Examples)

제너레이터 기반 예시는 PEP 342에 의존합니다. 또한, 일부 예시는 threading.RLock와 같은 적절한 객체가 with 문에서 직접 사용될 수 있으므로 실제로는 필요하지 않을 수 있습니다.

예시 컨텍스트의 이름에 사용된 시제는 임의적이지 않습니다. __enter__ 메서드에서 수행되고 __exit__ 메서드에서 되돌려지는 동작을 나타낼 때는 과거 시제(“ -ed “)가 사용됩니다. __exit__ 메서드에서 수행될 동작을 나타낼 때는 현재 진행형 시제(“ -ing “)가 사용됩니다.

1. 잠금(Lock) 보장: 블록 시작 시 획득한 잠금이 블록을 벗어날 때 해제되도록 보장하는 템플릿입니다.

@contextmanager
def locked(lock):
    lock.acquire()
    try:
        yield
    finally:
        lock.release()

# 사용 예시:
with locked(myLock):
    # 이 코드 블록은 myLock이 획득된 상태에서 실행됩니다.
    # 블록을 벗어날 때 (return 또는 잡히지 않은 예외를 통해서도)
    # 잠금은 해제되는 것이 보장됩니다.

2. 파일 열기 및 닫기 보장: 블록을 벗어날 때 파일이 닫히도록 보장하는 템플릿입니다.

@contextmanager
def opened(filename, mode="r"):
    f = open(filename, mode)
    try:
        yield f
    finally:
        f.close()

# 사용 예시:
with opened("/etc/passwd") as f:
    for line in f:
        print(line.rstrip())

3. 데이터베이스 트랜잭션 커밋 또는 롤백: 데이터베이스 트랜잭션을 커밋하거나 롤백하는 템플릿입니다.

@contextmanager
def transaction(db):
    db.begin()
    try:
        yield None
    except:
        db.rollback()
        raise
    else:
        db.commit()

4. locked 예시를 제너레이터 없이 재작성:

class locked:
    def __init__(self, lock):
        self.lock = lock
    def __enter__(self):
        self.lock.acquire()
    def __exit__(self, type, value, tb):
        self.lock.release()

5. 표준 출력(stdout) 일시적으로 리디렉션:

@contextmanager
def stdout_redirected(new_stdout):
    save_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield None
    finally:
        sys.stdout = save_stdout

# 사용 예시:
with opened(filename, "w") as f:
    with stdout_redirected(f):
        print("Hello world")

6. 오류 조건도 반환하는 opened() 변형:

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError as err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

# 사용 예시:
with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print("IOError:", err)
    else:
        f.write("guido::0:0::/:/bin/sh\n")

7. decimal 모듈의 추가 정밀도 컨텍스트:

import decimal
@contextmanager
def extra_precision(places=2):
    c = decimal.getcontext()
    saved_prec = c.prec
    c.prec += places
    try:
        yield None
    finally:
        c.prec = saved_prec

# 사용 예시:
def sin(x):
    with extra_precision():
        # ... 사인 계산 로직 ...
        return +s

8. decimal 모듈의 지역 컨텍스트 매니저:

from decimal import localcontext, ExtendedContext
@contextmanager
def localcontext(ctx=None):
    """Set a new local decimal context for the block"""
    # ... 구현 ...

# 사용 예시:
def sin(x):
    with localcontext() as ctx:
        ctx.prec += 2
        # ... 사인 계산 로직 ...
        return +s

9. 제네릭 “객체 닫기” 컨텍스트 매니저 (closing): close 메서드를 가진 모든 객체를 확정적으로 닫는 데 사용됩니다. (Python 2.5의 contextlib 모듈에 이 컨텍스트 매니저 버전이 포함되어 있습니다.)

class closing(object):
    def __init__(self, obj):
        self.obj = obj
    def __enter__(self):
        return self.obj
    def __exit__(self, *exc_info):
        try:
            close_it = self.obj.close
        except AttributeError:
            pass
        else:
            close_it()

# 사용 예시:
with closing(open("argument.txt")) as contradiction:
    for line in contradiction:
        print(line)

10. 잠금을 일시적으로 해제하는 released() 컨텍스트:

class released:
    def __init__(self, lock):
        self.lock = lock
    def __enter__(self):
        self.lock.release()
    def __exit__(self, type, value, tb):
        self.lock.acquire()

# 사용 예시:
with my_lock:
    # 잠금이 획득된 상태에서의 작업
    with released(my_lock):
        # 잠금 없이 작업
        # 예: 블로킹 I/O
    # 잠금이 다시 획득된 상태

11. 여러 컨텍스트를 자동으로 중첩하는 nested 컨텍스트 매니저: 과도한 들여쓰기를 피하기 위해 제공된 컨텍스트를 왼쪽에서 오른쪽으로 자동 중첩합니다. (Python 2.5의 contextlib 모듈에 이 컨텍스트 매니저 버전이 포함되어 있습니다.)

@contextmanager
def nested(*contexts):
    # ... 구현 ...

# 사용 예시:
with nested(a, b, c) as (x, y, z):
    # 작업 수행

# 위 코드는 다음 코드와 동일합니다.
with a as x:
    with b as y:
        with c as z:
            # 작업 수행

참조 구현 (Reference Implementation)

이 PEP는 2005년 6월 27일 EuroPython 기조연설에서 Guido에 의해 처음 수락되었습니다. __context__ 메서드가 추가된 후 다시 수락되었습니다. 이 PEP는 Python 2.5a1을 위해 Subversion에 구현되었고, __context__() 메서드는 Python 2.5b1에서 제거되었습니다.

감사의 글 (Acknowledgements)

PEP 340 및 PEP 346의 감사의 글에 언급된 모든 사람들을 포함하여, 이 PEP의 아이디어와 개념에 많은 사람들이 기여했습니다. 추가적으로 Paul Moore, Phillip J. Eby, Greg Ewing, Jason Orendorff, Michael Hudson, Raymond Hettinger, Walter Dörwald, Aahz, Georg Brandl, Terry Reedy, A.M. Kuchling, Brett Cannon, 그리고 python-dev 토론에 참여한 모든 사람들에게 감사드립니다.

참조 (References)

Raymond Chen의 숨겨진 흐름 제어에 대한 글 (https://devblogs.microsoft.com/oldnewthing/20050106-00/?p=36783) Guido가 PEP 342에 포함된 제너레이터 변경 사항을 제안 (https://mail.python.org/pipermail/python-dev/2005-May/053885.html) PEP 343에 대한 위키 토론 (http://wiki.python.org/moin/WithStatement) … (이하 생략)

이 문서는 퍼블릭 도메인에 공개되었습니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments