Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 346 – 사용자 정의 (”with”) 문

Author:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Withdrawn
Type:
Standards Track
Created:
06-May-2005
Python-Version:
2.5
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 PEP 310의 “신뢰할 수 있는 획득/해제 쌍(Reliable Acquisition/Release Pairs)”과 Guido의 PEP 340에 있는 “익명 블록 문(Anonymous Block Statements)”을 결합한 것입니다. 이 PEP는 PEP 340의 좋은 부분을 가져와 PEP 310의 일부와 섞고, 이를 모두 하나의 우아한 전체로 재구성하는 것을 목표로 합니다. 완전한 그림을 그리기 위해 여러 다른 PEP에서 내용을 빌려 왔으며, 그 자체로 독립적으로 성립하도록 작성되었습니다.

저자의 말

PEP 340에 대한 논의가 진행되는 동안, 저는 이 PEP의 초안을 PEP 3XX라는 이름으로 제 웹사이트에서 관리했습니다(python-dev의 활동을 따라갈 만큼 빠르게 제출된 PEP를 갱신할 수 있는 CVS 접근 권한이 없었기 때문입니다).

이 PEP의 첫 초안 이후, Guido는 PEP 340의 단순화된 버전으로 PEP 343을 작성했습니다. PEP 343은 (이 글을 쓰는 시점에서) 새로운 문에 대해 이 PEP와 정확히 같은 의미 체계를 사용하지만, 제너레이터를 사용해 문 템플릿을 작성할 수 있게 하는 메커니즘은 약간 다릅니다. 그러나 Guido는 Raymond Hettinger가 작성 중인 새 PEP를 수락할 의향이 있다고 밝혔습니다. 이 새 PEP는 PEP 288과 PEP 325를 통합하며, 이 PEP에서 설명하는 것과 같은 제너레이터 데코레이터를 사용해 PEP 343의 문 템플릿을 작성할 수 있도록 할 것입니다. 또 다른 차이점은 키워드의 선택(‘with’ 대 ‘do’)이었는데, Guido는 PEP 343의 맥락에서 이에 대한 투표를 진행하겠다고 밝혔습니다.

따라서 python.org에 보관용으로 제출된 이 PEP의 버전은 제출 직후 철회(WITHDRAWN)될 예정입니다. 중요한 아이디어는 PEP 343과 통합된 제너레이터 개선 PEP에서 다룰 것입니다.

소개

이 PEP는 임의의 try/finally 상용구 코드와 일부 try/except/else 상용구 코드를 분리해 낼 수 있게 하는 새로운 with 문을 도입하여, 자원을 신뢰성 있게 관리하는 Python의 능력을 향상할 것을 제안합니다. 이 새로운 구문을 ‘사용자 정의 문’이라고 하며, 관련된 클래스 정의를 ‘문 템플릿’이라고 합니다.

위의 내용이 이 PEP의 핵심입니다. 그러나 이 PEP가 말하는 것이 그것뿐이라면 PEP 310으로 충분할 것이고, 이 PEP는 본질적으로 불필요할 것입니다. 그 대신, 이 PEP는 적절하게 데코레이트된 제너레이터를 사용해 이러한 문 템플릿을 자연스럽게 작성할 수 있게 하는 추가 개선 사항을 권장합니다. 이러한 개선 사항의 부수 효과로, 제너레이터 내부에서 자원 관리를 적절히 처리하는 것이 중요해집니다.

이는 PEP 343과 상당히 비슷하지만, 발생한 예외가 제너레이터의 프레임 안에서 다시 발생하며, 그 결과 제너레이터 종료 처리 문제를 다루어야 합니다. 또한 이 PEP에서 제안하는 템플릿 제너레이터 데코레이터는 PEP 340의 일회용 템플릿이 아니라 재사용 가능한 템플릿을 만듭니다.

PEP 340과 비교하여, 이 PEP는 예외를 억제하는 기능을 제거하고, 사용자 정의 문을 반복하지 않는 구문으로 만듭니다. 또 다른 주요 차이점은 제너레이터를 문 템플릿으로 바꾸기 위해 데코레이터를 사용한다는 점과, 이터레이터 종료 처리를 다루기 위한 아이디어를 통합했다는 점입니다.

이 모든 것이 야심 찬 작업처럼 보인다면… 글쎄요, PEP 340을 작성하면서 기준을 그렇게 높게 설정한 사람은 Guido였습니다 :)

다른 PEP와의 관계

이 PEP는 PEP 310, PEP 340, PEP 343과 직접 경쟁합니다. 이 PEP들은 모두 결정론적 자원 관리를 처리하기 위한 대안적 메커니즘을 설명하기 때문입니다.

이 PEP는 PEP 340의 개선 사항 중 이터레이터에 데이터를 전달하는 것과 관련된 부분을 분리해 낸 PEP 342와는 경쟁하지 않습니다. for 루프 의미 체계에 대한 관련 변경 사항은 이 PEP에서 제안하는 이터레이터 종료 처리 변경 사항과 결합될 것입니다. 사용자 정의 문은 영향을 받지 않습니다.

이 PEP는 PEP 288에서 설명하는 제너레이터 개선 사항과도 경쟁하지 않습니다. 이 PEP는 제너레이터 프레임에 예외를 주입하는 기능을 제안하지만, 이는 내부 구현 세부 사항이며 그 기능을 Python 코드에 공개적으로 제공할 필요는 없습니다. PEP 288은 부분적으로 그 구현 세부 사항에 쉽게 접근할 수 있게 하는 것에 관한 것입니다.

그러나 이 PEP는 PEP 325에서 설명하는 제너레이터 자원 해제 지원을 불필요하게 만들 것입니다. 종료 처리가 필요한 이터레이터는 문 템플릿 프로토콜의 적절한 구현을 제공해야 합니다.

사용자 정의 문

PEP 310의 동기 부여 예제를 빌려 오면, 현재 동기화 잠금을 올바르게 처리하는 코드는 다음과 같습니다:

the_lock.acquire()
try:
    # Code here executes with the lock held
finally:
    the_lock.release()

PEP 310과 마찬가지로, 이 PEP는 그러한 코드를 다음과 같이 작성할 수 있게 하자고 제안합니다:

with the_lock:
    # Code here executes with the lock held

이러한 사용자 정의 문은 주로 함수로 쉽게 변환되지 않는 try 블록을 쉽게 분리해 낼 수 있도록 설계되었습니다. 이는 예외 처리 패턴은 일정하지만 try 블록의 본문이 바뀌는 경우에 가장 흔하게 해당합니다. 사용자 정의 문을 사용하면 예외 처리를 문 템플릿으로 간단히 분리해 내고, try 절의 본문은 사용자 코드 안에 인라인으로 제공할 수 있습니다.

‘사용자 정의 문’이라는 용어는 with 문의 의미가 주로 사용된 문 템플릿에 의해 결정되며, 프로그래머가 for 루프에서 사용할 자신만의 이터레이터를 자유롭게 만들 수 있는 것처럼 자신만의 문 템플릿도 자유롭게 만들 수 있다는 사실을 반영합니다.

사용자 정의 문의 사용 문법

제안하는 문법은 간단합니다:

with EXPR1 [as VAR1]:
    BLOCK1

사용자 정의 문의 의미

the_stmt = EXPR1
stmt_enter = getattr(the_stmt, "__enter__", None)
stmt_exit = getattr(the_stmt, "__exit__", None)
if stmt_enter is None or stmt_exit is None:
    raise TypeError("Statement template required")

VAR1 = stmt_enter() # Omit 'VAR1 =' if no 'as' clause
exc = (None, None, None)
try:
    try:
        BLOCK1
    except:
        exc = sys.exc_info()
        raise
finally:
    stmt_exit(*exc)

VAR1을 제외하면, 위에 보인 지역 변수 중 어느 것도 사용자 코드에서 보이지 않습니다. for 루프의 반복 변수와 마찬가지로, VAR1은 BLOCK1과 사용자 정의 문 뒤에 오는 코드 양쪽에서 모두 보입니다.

문 템플릿은 예외에 반응할 수만 있고 예외를 억제할 수는 없다는 점에 유의하십시오. 그 이유에 대한 설명은 Rejected Options 절을 참조하십시오.

문 템플릿 프로토콜: __enter__

__enter__() 메서드는 인자를 받지 않으며, 이 메서드가 예외를 발생시키면 BLOCK1은 결코 실행되지 않습니다. 이 경우 __exit__() 메서드는 호출되지 않습니다. as 절을 사용하면 이 메서드가 반환한 값이 VAR1에 대입됩니다. 달리 반환할 값이 없는 객체는 with 문 안에서 즉석 생성이 가능하도록 일반적으로 None이 아니라 self를 반환해야 합니다.

문 템플릿은 이 메서드를 사용하여 문이 실행되는 동안 유지되어야 하는 조건(예: 동기화 잠금 획득)을 설정해야 합니다.

항상 사용할 수 있는 것은 아닌 문 템플릿(예: 닫힌 파일 객체)은 템플릿이 유효한 상태가 아닐 때 __enter__()를 호출하려고 시도하면 RuntimeError를 발생시켜야 합니다.

문 템플릿 프로토콜: __exit__

__exit__() 메서드는 raise 문의 세 “인자”인 타입, 값, 트레이스백에 대응하는 세 개의 인자를 받습니다. 모든 인자는 항상 전달되며, 예외가 발생하지 않았다면 None으로 설정됩니다. __enter__() 메서드가 성공적으로 완료되면, 이 메서드는 with 문 메커니즘에 의해 정확히 한 번 호출됩니다.

문 템플릿은 이 메서드에서 예외 처리를 수행합니다. 첫 번째 인자가 None이면 BLOCK1이 예외 없이 완료되었음을 나타냅니다. 즉, 실행이 블록의 끝에 도달했거나 return, break 또는 continue 문을 사용하여 조기 완료가 강제된 경우입니다. 그렇지 않으면, 세 인자는 BLOCK1을 종료시킨 예외를 반영합니다.

__exit__() 메서드가 발생시킨 모든 예외는 with 문을 포함하는 스코프로 전파됩니다. BLOCK1 안의 사용자 코드도 예외를 발생시켰다면, 그 예외는 사라지고 __exit__() 메서드가 발생시킨 예외로 대체됩니다.

임의의 예외 처리 분리해 내기

다음과 같은 예외 처리 구성을 생각해 보십시오:

SETUP_BLOCK
try:
    try:
        TRY_BLOCK
    except exc_type1, exc:
        EXCEPT_BLOCK1
    except exc_type2, exc:
        EXCEPT_BLOCK2
    except:
        EXCEPT_BLOCK3
    else:
        ELSE_BLOCK
finally:
    FINALLY_BLOCK

이는 대략 다음과 같이 문 템플릿으로 옮길 수 있습니다:

class my_template(object):

    def __init__(self, *args):
        # Any required arguments (e.g. a file name)
        # get stored in member variables
        # The various BLOCK's will need updating to reflect
        # that.

    def __enter__(self):
        SETUP_BLOCK

    def __exit__(self, exc_type, value, traceback):
        try:
            try:
                if exc_type is not None:
                    raise exc_type, value, traceback
            except exc_type1, exc:
                EXCEPT_BLOCK1
            except exc_type2, exc:
                EXCEPT_BLOCK2
            except:
                EXCEPT_BLOCK3
            else:
                ELSE_BLOCK
        finally:
            FINALLY_BLOCK

그러면 다음과 같이 사용할 수 있습니다:

with my_template(*args):
    TRY_BLOCK

그러나 이 코드와 원래의 try 문 사이에는 두 가지 중요한 의미상의 차이가 있습니다.

첫째, 원래의 try 문에서는 TRY_BLOCK에서 break, return 또는 continue 문을 만나면 문이 완료될 때 FINALLY_BLOCK만 실행됩니다. 문 템플릿에서는 이러한 문이 예외가 아닌 다른 블록 종료와 똑같이 취급되므로 ELSE_BLOCK도 실행됩니다. 이것이 중요한 사용 사례에서는 이는 좋은 일일 가능성이 높습니다(Examples의 transaction을 참고하십시오). except 절과 else 절 어느 쪽도 실행되지 않는 이 허점은 예외 핸들러를 작성할 때 잊어버리기 쉽기 때문입니다.

둘째, 문 템플릿은 어떤 예외도 억제하지 않습니다. 예를 들어 원래 코드가 exc_type1과 exc_type2 예외를 억제했다면, 이는 여전히 사용자 코드 안에서 직접 처리해야 합니다:

try:
    with my_template(*args):
        TRY_BLOCK
except (exc_type1, exc_type2):
    pass

그러나 예외 억제를 명시적으로 해야 하는 이러한 경우에도 호출하는 쪽에서 반복되는 상용구 코드의 양은 크게 줄어듭니다(이 동작에 대한 자세한 논의는 Rejected Options를 참고하십시오).

일반적으로 모든 절이 필요한 것은 아닙니다. (파일이나 동기화 잠금 같은) 리소스를 처리할 때는 FINALLY_BLOCK에 들어갔을 코드를 __exit__() 메서드에서 그냥 실행하면 됩니다. 이 절의 시작 부분에서 언급했듯이 동기화 잠금을 문 템플릿으로 만드는 다음 구현에서 이를 확인할 수 있습니다:

# New methods of synchronisation lock objects

def __enter__(self):
    self.acquire()
    return self

def __exit__(self, *exc_info):
    self.release()

제너레이터

제너레이터는 실행을 일시 중단하고 호출한 프레임으로 제어를 되돌릴 수 있으므로 문 템플릿을 작성하기에 자연스러운 후보입니다. 사용자 정의 문을 언어에 추가하는 데 이 절에서 설명하는 제너레이터 변경이 필요한 것은 아니므로, 이 PEP는 단계적 구현(1단계에서 with 문, 2단계에서 제너레이터 통합)의 명백한 후보가 됩니다. 제안된 제너레이터 개선을 통해 다음과 같이 임의의 예외 처리를 분리해 낼 수 있습니다:

@statement_template
def my_template(*arguments):
    SETUP_BLOCK
    try:
        try:
            yield
        except exc_type1, exc:
            EXCEPT_BLOCK1
        except exc_type2, exc:
            EXCEPT_BLOCK2
        except:
            EXCEPT_BLOCK3
        else:
            ELSE_BLOCK
    finally:
        FINALLY_BLOCK

클래스 기반 버전과 달리 어떤 블록도 수정할 필요가 없다는 점에 유의하십시오. 호출하는 코드가 전달한 인자를 포함하여 공유되는 값은 제너레이터 내부 프레임의 지역 변수이기 때문입니다. 앞서 언급한 의미상의 차이(예외가 아닌 모든 블록 종료가 else 절을 실행시키며, 템플릿은 예외를 억제할 수 없음)는 여전히 적용됩니다.

yield의 기본값

제너레이터로 문 템플릿을 만들 때 yield 문은 유용한 값을 반환하기보다는 사용자 정의 문의 본문으로 제어를 되돌리는 용도로만 쓰이는 경우가 많습니다.

따라서 이 PEP가 채택되면 yield는 return과 마찬가지로 None을 기본값으로 제공합니다(즉, yield와 yield None은 동등한 문이 됩니다).

이와 같은 변경이 PEP 342에서도 제안되고 있습니다. 당연히 두 PEP가 모두 채택된다면 한 번만 구현하면 됩니다 :)

템플릿 제너레이터 데코레이터: statement_template

PEP 343과 마찬가지로, 제너레이터를 적절한 문 템플릿 의미 체계를 갖는 객체로 감싸는 새로운 데코레이터를 제안합니다. PEP 343과 달리 여기서 제안하는 템플릿은 재사용할 수 있습니다. __enter__()를 호출할 때마다 제너레이터가 새로 인스턴스화되기 때문입니다. 또한 BLOCK1에서 발생한 모든 예외는 제너레이터의 내부 프레임에서 다시 발생합니다:

class template_generator_wrapper(object):

    def __init__(self, func, func_args, func_kwds):
         self.func = func
         self.args = func_args
         self.kwds = func_kwds
         self.gen = None

    def __enter__(self):
        if self.gen is not None:
            raise RuntimeError("Enter called without exit!")
        self.gen = self.func(*self.args, **self.kwds)
        try:
            return self.gen.next()
        except StopIteration:
            raise RuntimeError("Generator didn't yield")

    def __exit__(self, *exc_info):
        if self.gen is None:
            raise RuntimeError("Exit called without enter!")
        try:
            try:
                if exc_info[0] is not None:
                    self.gen._inject_exception(*exc_info)
                else:
                    self.gen.next()
            except StopIteration:
                pass
            else:
                raise RuntimeError("Generator didn't stop")
        finally:
            self.gen = None

def statement_template(func):
    def factory(*args, **kwds):
        return template_generator_wrapper(func, args, kwds)
    return factory

템플릿 제너레이터 래퍼: __enter__() 메서드

템플릿 제너레이터 래퍼에는 __enter__() 메서드가 있으며, 이 메서드는 포함된 제너레이터의 새 인스턴스를 만든 다음 next()를 한 번 호출합니다. 마지막 제너레이터 인스턴스가 정리되지 않았거나 제너레이터가 값을 산출하지 않고 종료되면 RuntimeError를 발생시킵니다.

템플릿 제너레이터 래퍼: __exit__() 메서드

템플릿 제너레이터 래퍼에는 __exit__() 메서드가 있으며, 이 메서드는 예외가 전달되지 않으면 제너레이터에 대해 next()를 호출하기만 합니다. 예외가 전달되면 포함된 제너레이터의 마지막 yield 문 지점에서 그 예외를 다시 발생시킵니다.

어느 경우든 해당 작업의 결과로 내부 프레임이 종료되지 않으면 제너레이터 래퍼는 RuntimeError를 발생시킵니다. __exit__() 메서드는 사용된 제너레이터 인스턴스에 대한 참조를 항상 정리하므로 __enter__()를 다시 호출할 수 있습니다.

사용자 정의 문의 본문에서 발생한 StopIteration이 __exit__() 메서드 안에서 의도치 않게 억제될 수 있습니다. 하지만 원래 발생한 예외는 여전히 올바르게 전파되므로 이는 중요하지 않습니다.

제너레이터에 예외 주입하기

템플릿 제너레이터 래퍼의 __exit__() 메서드를 구현하려면 제너레이터의 내부 프레임에 예외를 주입해야 합니다. 이는 현재 Python에 대응하는 기능이 없는 새로운 구현 수준의 동작입니다.

주입 메커니즘(이 PEP에서는 _inject_exception이라고 부릅니다)은 지정된 타입, 값, 트레이스백 정보로 제너레이터의 프레임에서 예외를 발생시킵니다. 따라서 예외가 전파되도록 허용되면 그 예외는 원래 예외와 똑같이 보입니다.

이 PEP의 목적상 이 기능을 Python 구현 코드 외부에서 사용할 수 있게 만들 필요는 없습니다.

제너레이터 종료 처리

템플릿 제너레이터에서 자원 관리를 지원하기 위해 이 PEP는 try/finally 문의 try 블록 안에서 yield 문을 사용할 수 없다는 제약을 없앱니다. 따라서 파일이나 그와 비슷한 객체를 사용해야 하는 제너레이터는 try/finally 문이나 with 문을 사용하여 해당 객체가 올바르게 관리되도록 보장할 수 있습니다.

이 제약은 전역적으로 해제해야 할 가능성이 높습니다. 문 템플릿을 정의하는 데 사용되는 제너레이터 안에서만 허용되도록 제한하기는 어려울 것이기 때문입니다. 따라서 이 PEP에는 문 템플릿으로 사용되지 않는 제너레이터도 여전히 적절하게 종료 처리되도록 보장하기 위한 제안이 포함되어 있습니다.

제너레이터 종료 처리: TerminateIteration 예외

새로운 예외를 제안합니다:

class TerminateIteration(Exception): pass

새로운 예외는 종료 처리를 요청하기 위해 제너레이터에 주입됩니다. 올바르게 동작하는 코드는 이 예외를 억제해서는 안 됩니다.

제너레이터 종료 처리: __del__() 메서드

제너레이터가 (Python의 가비지 컬렉션이 허용하는 범위 내에서) 결국 종료 처리되도록 보장하기 위해 제너레이터는 다음과 같은 의미를 갖는 __del__() 메서드를 갖게 됩니다:

def __del__(self):
    try:
        self._inject_exception(TerminateIteration, None, None)
    except TerminateIteration:
        pass

결정적인 제너레이터 종료 처리

제너레이터의 결정적인 종료 처리를 제공하는 간단한 방법이 있습니다. 제너레이터에 적절한 __enter__() 메서드와 __exit__() 메서드를 부여하는 것입니다:

def __enter__(self):
    return self

def __exit__(self, *exc_info):
    try:
        self._inject_exception(TerminateIteration, None, None)
    except TerminateIteration:
        pass

그러면 관련 for 루프를 with 문으로 감싸서 어떤 제너레이터든 즉시 종료 처리할 수 있습니다:

with all_lines(filenames) as lines:
    for line in lines:
        print lines

(all_lines의 정의와 이것에 즉각적인 종료 처리가 필요한 이유는 Examples를 참조하십시오)

위 예제를 파일 객체의 사용법과 비교해 보십시오:

with open(filename) as f:
    for line in f:
        print f

사용자 정의 문 템플릿으로서의 제너레이터

사용자 정의 문을 구현하는 데 사용될 때 제너레이터는 주어진 제어 경로에서 한 번만 yield해야 합니다. 그러면 그 yield의 결과가 제너레이터의 __enter__() 메서드의 결과로 제공됩니다. 각 제어 경로에 yield가 하나만 있으면 제너레이터의 __exit__() 메서드가 호출될 때 내부 프레임이 반드시 종료됩니다. 하나의 제어 경로에 yield 문이 여러 개 있으면 내부 프레임이 올바르게 종료되지 못할 때 __exit__() 메서드가 RuntimeError를 발생시킵니다. 이러한 오류는 문 템플릿에 버그가 있음을 나타냅니다.

예외에 대응하거나 리소스를 정리하려면 yield 문을 적절하게 구성한 try 문으로 감싸기만 하면 됩니다. yield 이후에 예외 없이 실행이 재개되면, 제너레이터는 do 문의 본문이 아무 문제 없이 완료되었음을 알 수 있습니다.

예시

  1. 블록 시작 시 획득한 잠금이 블록을 벗어날 때 해제되도록 보장하는 템플릿:
    # New methods on synchronisation locks
        def __enter__(self):
            self.acquire()
            return self
    
        def __exit__(self, *exc_info):
            lock.release()
    

    다음과 같이 사용합니다:

    with myLock:
        # Code here executes with myLock held.  The lock is
        # guaranteed to be released when the block is left (even
        # if via return or by an uncaught exception).
    
  2. 파일을 열고, 블록을 벗어날 때 파일이 닫히도록 보장하는 템플릿:
    # New methods on file objects
        def __enter__(self):
            if self.closed:
                raise RuntimeError, "Cannot reopen closed file handle"
            return self
    
        def __exit__(self, *args):
            self.close()
    

    다음과 같이 사용합니다:

    with open("/etc/passwd") as f:
        for line in f:
            print line.rstrip()
    
  3. 데이터베이스 트랜잭션을 커밋하거나 롤백하는 템플릿:
    def transaction(db):
        try:
            yield
        except:
            db.rollback()
        else:
            db.commit()
    

    다음과 같이 사용합니다:

    with transaction(the_db):
        make_table(the_db)
        add_data(the_db)
        # Getting to here automatically triggers a commit
        # Any exception automatically triggers a rollback
    
  4. 블록을 중첩하고 템플릿을 조합할 수 있습니다:
    @statement_template
    def lock_opening(lock, filename, mode="r"):
        with lock:
            with open(filename, mode) as f:
                yield f
    

    다음과 같이 사용합니다:

    with lock_opening(myLock, "/etc/passwd") as f:
        for line in f:
            print line.rstrip()
    
  5. stdout을 일시적으로 리디렉션합니다:
    @statement_template
    def redirected_stdout(new_stdout):
        save_stdout = sys.stdout
        try:
            sys.stdout = new_stdout
            yield
        finally:
            sys.stdout = save_stdout
    

    다음과 같이 사용합니다:

    with open(filename, "w") as f:
        with redirected_stdout(f):
            print "Hello world"
    
  6. 오류 상태도 함께 반환하는 open()의 변형:
    @statement_template
    def open_w_error(filename, mode="r"):
        try:
            f = open(filename, mode)
        except IOError, err:
            yield None, err
        else:
            try:
                yield f, None
            finally:
                f.close()
    

    다음과 같이 사용합니다:

    do open_w_error("/etc/passwd", "a") as f, err:
        if err:
            print "IOError:", err
        else:
            f.write("guido::0:0::/:/bin/sh\n")
    
  7. 특정 헤더가 있는 첫 번째 파일을 찾습니다:
    for name in filenames:
        with open(name) as f:
            if f.read(2) == 0xFEB0:
                break
    
  8. 처리할 수 있는 첫 번째 항목을 찾되, 루프 전체 동안 또는 각 반복 동안에만 잠금을 유지합니다:
    with lock:
        for item in items:
            if handle(item):
                break
    
    for item in items:
        with lock:
            if handle(item):
                break
    
  9. 제너레이터 내부에 있는 동안 잠금을 유지하되, 바깥 스코프로 제어를 반환할 때는 잠금을 해제합니다:
    @statement_template
    def released(lock):
        lock.release()
        try:
            yield
        finally:
            lock.acquire()
    

    다음과 같이 사용합니다:

    with lock:
        for item in items:
            with released(lock):
                yield item
    
  10. 여러 파일에서 줄을 읽습니다(예: 여러 설정 소스를 처리하는 경우):
    def all_lines(filenames):
        for name in filenames:
            with open(name) as f:
                for line in f:
                    yield line
    

    다음과 같이 사용합니다:

    with all_lines(filenames) as lines:
        for line in lines:
            update_config(line)
    
  11. 모든 용도가 리소스 관리와 관련될 필요는 없습니다:
    @statement_template
    def tag(*args, **kwds):
        name = cgi.escape(args[0])
        if kwds:
            kwd_pairs = ["%s=%s" % cgi.escape(key), cgi.escape(value)
                         for key, value in kwds]
            print '<%s %s>' % name, " ".join(kwd_pairs)
        else:
            print '<%s>' % name
        yield
        print '</%s>' % name
    

    다음과 같이 사용합니다:

    with tag('html'):
        with tag('head'):
           with tag('title'):
              print 'A web page'
        with tag('body'):
           for par in pars:
              with tag('p'):
                 print par
           with tag('a', href="http://www.python.org"):
               print "Not a dead parrot!"
    
  12. PEP 343에서 가져온 또 다른 유용한 예는 시그널을 차단하는 연산입니다. 사용법은 다음과 같을 수 있습니다:
    from signal import blocked_signals
    
    with blocked_signals():
        # code executed without worrying about signals
    

    선택적 인자로 차단할 시그널의 리스트를 받을 수 있으며, 기본적으로는 모든 시그널이 차단됩니다. 구현은 독자의 연습 문제로 남겨 둡니다.

  13. 이 기능의 또 다른 용도는 Decimal 컨텍스트입니다:
    # New methods on decimal Context objects
    
    def __enter__(self):
        if self._old_context is not None:
            raise RuntimeError("Already suspending other Context")
        self._old_context = getcontext()
        setcontext(self)
    
    def __exit__(self, *args):
        setcontext(self._old_context)
        self._old_context = None
    

    다음과 같이 사용합니다:

    with decimal.Context(precision=28):
       # Code here executes with the given context
       # The context always reverts after this statement
    

미해결 문제

이 PEP는 철회되었으므로 없습니다.

기각된 선택지

기본 구문을 루프 구문으로 만들기

PEP 340의 block 문에서 볼 수 있듯이, 이 아이디어의 주요 문제는 루프 안에 있으면서 break 및 continue 문을 포함하는 try 문을 분리해 낼 때 문제가 생긴다는 점입니다(이러한 문이 원래 루프가 아니라 block 구문에 적용되기 때문입니다). (억제를 제외한) 임의의 예외 처리를 문 템플릿으로 분리해 낼 수 있게 하는 것이 핵심 목표이므로, 이는 명백한 문제입니다.

Examples에서 볼 수 있듯이 이해하기 어렵다는 문제도 있습니다. 루프 전체에 대해 잠금을 획득하거나 루프의 각 반복마다 잠금을 획득하는 예제에서, 사용자 정의 문 자체가 루프라면 이를 for 루프 바깥에서 for 루프 안으로 옮길 때 예상할 수 있는 범위를 넘어서는 중대한 의미적 영향이 발생합니다.

마지막으로, 루프 구문을 사용하면 TOOWTDI와 관련하여 심각한 문제가 생깁니다. 특정 상황을 전통적인 for 루프로 처리해야 하는지 새로운 루프 구문으로 처리해야 하는지 불분명한 경우가 많기 때문입니다. 현재 PEP에는 그러한 문제가 없습니다. for 루프는 계속 반복에 사용되고, 새로운 do 문은 예외 처리를 분리해 내는 데 사용됩니다.

특히 PEP 340의 익명 블록 문과 관련된 또 다른 문제는 문 템플릿을 직접(즉, 제너레이터를 사용하지 않고) 작성하기가 상당히 어려워진다는 점입니다. Examples에 있는 여러 클래스 기반 문 템플릿 구현이 비교적 단순하다는 점에서 볼 수 있듯이, 현재 제안은 이 문제를 해결합니다.

문 템플릿이 예외를 억제하도록 허용하기

이 PEP의 이전 버전에서는 문 템플릿에 예외를 억제하는 기능을 부여했습니다. BDFL은 그에 따르는 복잡성에 우려를 표했으며, 저는 C 코드에서 매크로 안에 흐름 제어를 숨기는 것의 폐해에 관한 Raymond Chen의 글 [1]을 읽고 나서 이에 동의했습니다.

억제 기능을 제거하자 사용자 정의 문의 설명과 구현 양쪽에서 엄청난 복잡성이 사라졌으며, 이는 그것이 올바른 선택이라는 점을 더욱 뒷받침합니다. 이전 버전의 PEP에서는 __exit__() 메서드에서 의도치 않게 예외를 억제하는 일을 피하기 위해 끔찍할 정도로 복잡한 우회 방법을 거쳐야 했지만, 현재 제안된 의미 체계에서는 그러한 문제가 존재하지 않습니다.

예외를 억제하는 기능을 실제로 사용한 예제가 하나(auto_retry) 있었습니다. 이 사용 사례는 그다지 우아하지는 않지만, 사용자 코드에 전부 풀어 쓰면 제어 흐름이 훨씬 더 명확해집니다:

def attempts(num_tries):
    return reversed(xrange(num_tries))

for retry in attempts(3):
    try:
        make_attempt()
    except IOError:
        if not retry:
            raise

참고로, 별난 취향을 가진 사람이라면 여전히 이것을 다음과 같이 작성할 수 있습니다:

for attempt in auto_retry(3, IOError):
    try:
        with attempt:
            make_attempt()
    except FailedAttempt:
        pass

무고한 사람들을 보호하기 위해, 이를 실제로 지원하는 코드는 여기에 포함하지 않았습니다.

예외가 아닌 종료를 구별하기

이 PEP의 이전 버전에서는 문 템플릿이 블록을 정상적으로 빠져나가는 경우와 return, break 또는 continue 문을 통해 빠져나가는 경우를 구별할 수 있었습니다. BDFL도 PEP 343 및 관련 논의에서 비슷한 아이디어를 검토한 적이 있습니다. 이로 인해 의미 체계에 대한 설명이 상당히 복잡해졌으며, 모든 문 템플릿이 각각 그러한 문을 예외처럼 취급할지, 아니면 블록을 빠져나가는 정상적인 메커니즘처럼 취급할지를 결정해야 했습니다.

이처럼 템플릿마다 결정하는 방식은 큰 혼란을 초래할 가능성이 있었습니다. 한 데이터베이스 커넥터는 조기 종료를 예외처럼 취급하는 트랜잭션 템플릿을 제공하는 반면, 다른 커넥터는 이를 정상적인 블록 종료로 취급하는 경우를 생각해 보십시오.

따라서 이 PEP는 이제 가장 단순한 해결책을 사용합니다. 문 템플릿의 입장에서 조기 종료는 정상적인 블록 종료와 동일하게 보입니다.

발생한 예외를 제너레이터에 주입하지 않기

PEP 343에서는 문 템플릿을 정의하는 데 사용되는 제너레이터에 대해 단순히 무조건 next()를 호출할 것을 제안합니다. 이 때문에 템플릿 제너레이터가 상당히 직관적이지 않은 모습이 되며, try/finally 안에서 yield하는 것을 금지하는 규칙이 유지되므로 여러 리소스를 관리하는 데 Python의 예외 처리 기능을 사용할 수 없게 됩니다.

이 PEP가 지지하는 대안(발생한 예외를 제너레이터 프레임에 주입하는 방식)을 사용하면, Examples의 lock_opening에서 보듯이 여러 리소스를 우아하게 관리할 수 있습니다

모든 제너레이터를 문 템플릿으로 만들기

템플릿 객체를 제너레이터 자체와 분리하면 재사용 가능한 제너레이터 템플릿을 만들 수 있습니다. 즉, 이 PEP가 채택되면 다음 코드가 올바르게 동작합니다:

open_it = lock_opening(parrot_lock, "dead_parrot.txt")

with open_it as f:
    # use the file for a while

with open_it as f:
    # use the file again

두 번째 이점은 이터레이터 제너레이터와 템플릿 제너레이터가 매우 다른 것이라는 점입니다. 데코레이터는 그 구분을 명확하게 유지하고, 한쪽이 필요한 곳에 다른 쪽이 사용되는 것을 방지합니다.

마지막으로, 데코레이터를 필수로 하면 제너레이터 객체의 네이티브 메서드를 사용해 제너레이터 종료 처리를 구현할 수 있습니다.

do를 키워드로 사용하기

do는 PEP 340 논의 중에 제안된 대안 키워드였습니다. 적절한 이름의 함수와 함께 쓰면 잘 읽히지만, 메서드나 네이티브 문 템플릿 지원을 제공하는 객체와 함께 쓰면 잘 읽히지 않습니다.

do가 처음 제안되었을 때, BDFL은 Pascal/Delphi 스타일의 with 문에 쓰고 싶다는 이유로 PEP 310의 with 키워드를 거부한 상태였습니다. 그 이후 BDFL은 더 이상 그러한 문을 제공할 생각이 없어졌으므로 이 반대를 철회했습니다. 이러한 생각의 변화는 C# 개발자들이 해당 기능을 제공하지 않은 이유에 근거한 것으로 보입니다 [2].

키워드를 두지 않기

이는 흥미로운 선택지이며, 꽤 잘 읽히도록 만들 수 있습니다. 그러나 새로운 사용자가 문서에서 찾아보기 어렵고, 일부 사람들에게는 지나치게 마법처럼 느껴집니다. 따라서 이 PEP는 키워드 기반 제안을 채택합니다.

try 문 확장하기

이 제안은 아무 절도 붙지 않은 try 문에 with 문에 제안된 것과 유사한 시그니처를 부여하는 것입니다.

저는 with 문을 확장된 try 문으로 작성하려는 것이 for 루프를 확장된 while 루프로 작성하려는 것만큼이나 이치에 맞지 않는다고 생각합니다. 즉, 전자의 의미론을 후자를 사용하는 특정한 방식으로 설명할 수는 있지만, 전자가 후자의 인스턴스인 것은 아닙니다. 더 근본적인 문 주위에 추가된 의미론은 새로운 구문을 만들어 내며, 서로 다른 두 문을 혼동해서는 안 됩니다.

이는 ‘확장된’ try 문도 여전히 ‘확장되지 않은’ try 문의 관점에서 설명해야 한다는 사실에서 알 수 있습니다. 무언가 다른 것이라면, 다른 이름을 붙이는 편이 더 합리적입니다.

템플릿 프로토콜이 try 문을 직접 반영하도록 하기

한 가지 제안은 일반화된 try 문 구조의 서로 다른 부분을 다루도록 프로토콜에 별도의 메서드를 두자는 것이었습니다. try, except, else, finally라는 용어를 사용하면, 다음과 같은 형태가 됩니다:

class my_template(object):

    def __init__(self, *args):
        # Any required arguments (e.g. a file name)
        # get stored in member variables
        # The various BLOCK's will need to updated to reflect
        # that.

    def __try__(self):
        SETUP_BLOCK

    def __except__(self, exc, value, traceback):
        if isinstance(exc, exc_type1):
            EXCEPT_BLOCK1
        if isinstance(exc, exc_type2):
            EXCEPT_BLOCK2
        else:
            EXCEPT_BLOCK3

    def __else__(self):
        ELSE_BLOCK

    def __finally__(self):
        FINALLY_BLOCK

메서드 슬롯을 네 개보다 두 개 추가하는 편을 선호한다는 점 외에도, 저는 기능을 여러 다른 메서드에 나누어야 하는 것(또는 템플릿이 모든 절을 사용하지 않는 경우 어떤 메서드를 사용할지 파악해야 하는 것)보다, (Factoring out arbitrary exception handling에서 보인 것처럼) 원래의 try 문 코드를 약간 수정한 버전을 __exit__() 메서드에 그대로 재현할 수 있는 편이 훨씬 쉽다고 생각합니다.

이 논의를 덜 이론적으로 만들기 위해, 제너레이터 대신 두 메서드 프로토콜과 네 메서드 프로토콜을 각각 사용해 구현한 transaction 예제를 소개합니다. 두 구현 모두 break, return, continue 문을 만나면 커밋을 보장합니다(Examples 섹션의 제너레이터 기반 구현도 마찬가지입니다):

class transaction_2method(object):

    def __init__(self, db):
        self.db = db

    def __enter__(self):
        pass

    def __exit__(self, exc_type, *exc_details):
        if exc_type is None:
            self.db.commit()
        else:
            self.db.rollback()

class transaction_4method(object):

    def __init__(self, db):
        self.db = db
        self.commit = False

    def __try__(self):
        self.commit = True

    def __except__(self, exc_type, exc_value, traceback):
        self.db.rollback()
        self.commit = False

    def __else__(self):
        pass

    def __finally__(self):
        if self.commit:
            self.db.commit()
            self.commit = False

제안에 포함된 구체적인 메서드 이름과 관련하여 사소한 사항이 두 가지 더 있습니다. SETUP_BLOCK은 try 문에 진입하기 전에 실행되므로 __try__() 메서드의 이름은 오해의 소지가 있으며, 다른 많은 Python 문에 else 절이 있으므로 __else__() 메서드의 이름은 단독으로 보았을 때 의미가 불분명합니다.

이터레이터 종료 처리 (철회됨)

제너레이터 내부에서 사용자 정의 문을 사용할 수 있게 되면, 자원 관리가 지금처럼 외부에서 처리되지 않고 제너레이터 내부로 옮겨지므로 이터레이터의 결정적 종료 처리에 대한 필요성이 커질 가능성이 높습니다.

현재 이 PEP는 모든 제너레이터를 문 템플릿으로 만들고 with 문을 사용해 종료 처리를 다루는 방식으로 이를 해결할 것을 제안합니다. 그러나 이 PEP의 이전 버전에서는 제너레이터의 작성자가 종료 처리의 필요성을 플래그로 표시하고 for 루프가 이를 자동으로 처리하도록 하는, 다음과 같은 더 복잡한 해결책을 제안했습니다. 이 해결책은 거부된 선택지로서 여기에 길고 자세하게 수록합니다.

이터레이터 프로토콜 추가 사항: __finish__

이터레이터에 선택적으로 구현할 수 있는 새 메서드인 __finish__()를 제안합니다. 이 메서드는 인자를 받지 않으며, 아무것도 반환하지 않아야 합니다.

__finish__ 메서드는 이터레이터가 열어 둔 모든 자원을 정리해야 합니다. 이 PEP의 나머지 부분에서는 __finish__() 메서드를 가진 이터레이터를 ‘종료 가능한 이터레이터(finishable iterator)’라고 부릅니다.

최선의 노력에 따른 종료 처리

종료 가능한 이터레이터는 (예를 들어 __finish__() 메서드를 호출하는 방식으로) 종료 처리를 함께 수행하는 __del__ 메서드를 반드시 제공해야 합니다. 이렇게 하면 이터레이터에 결정적 종료 처리가 적용되지 않는 경우에도 Python이 최선을 다해 종료 처리를 시도할 수 있습니다.

결정적 종료 처리

for 루프에서 사용하는 이터레이터에 __finish__() 메서드가 있으면, 개선된 for 루프 의미 체계는 루프를 빠져나가는 방식과 관계없이 해당 메서드가 실행되도록 보장합니다. 이는 User defined statements나 이제 허용되는 try/finally 문을 활용하는 이터레이터 제너레이터에 중요하며, 할당된 자원을 해제하기 위해 적시의 종료 처리에 의존하는 새로운 이터레이터(예: 스레드나 데이터베이스 연결을 풀에 반환하는 경우)에도 중요합니다.

for 루프 문법

for 루프 문법에 대해서는 어떤 변경도 제안하지 않습니다. 이는 단지 의미 체계를 설명하는 데 필요한 문의 구성 부분을 정의하기 위한 것입니다:

for VAR1 in EXPR1:
    BLOCK1
else:
    BLOCK2

갱신된 for 루프 의미 체계

대상 이터레이터에 __finish__() 메서드가 없으면 for 루프는 다음과 같이 실행됩니다(즉, 현재와 달라지는 것이 없습니다):

itr = iter(EXPR1)
exhausted = False
while True:
    try:
        VAR1 = itr.next()
    except StopIteration:
        exhausted = True
        break
    BLOCK1
if exhausted:
    BLOCK2

대상 이터레이터에 __finish__() 메서드가 있으면 for 루프는 다음과 같이 실행됩니다:

itr = iter(EXPR1)
exhausted = False
try:
    while True:
        try:
            VAR1 = itr.next()
        except StopIteration:
            exhausted = True
            break
        BLOCK1
    if exhausted:
        BLOCK2
finally:
    itr.__finish__()

구현 시에는 이터레이터에 __finish__() 메서드가 없을 때 try/finally 오버헤드가 발생하지 않도록 주의를 기울여야 합니다.

제너레이터 이터레이터 종료 처리: __finish__() 메서드

적절한 데코레이터로 활성화하면, 제너레이터는 내부 프레임에서 TerminateIteration을 발생시키는 __finish__() 메서드를 갖게 됩니다:

def __finish__(self):
    try:
        self._inject_exception(TerminateIteration)
    except TerminateIteration:
        pass

이 기능을 활성화하려면 데코레이터(예: needs_finish())가 필요하며, 이는 종료 처리를 예상하지 않는 기존 제너레이터가 예상대로 계속 동작하도록 하기 위함입니다.

종료 가능한 이터레이터의 부분 순회

종료 가능한 이터레이터의 부분 순회는 가능하지만, 이터레이터가 여전히 신속하게 종료 처리되도록 보장하려면 약간의 주의가 필요합니다(애초에 이유가 있어서 종료 가능하게 만든 것이기 때문입니다!). 먼저, 이터레이터의 __finish__() 메서드를 for 루프로부터 숨겨서 종료 가능한 이터레이터의 부분 순회를 가능하게 하는 클래스가 필요합니다:

class partial_iter(object):

    def __init__(self, iterable):
        self.iter = iter(iterable)

    def __iter__(self):
        return self

    def next(self):
        return self.itr.next()

다음으로, 이터레이터가 결국 종료되도록 보장하는 적절한 문 템플릿이 필요합니다:

@statement_template
def finishing(iterable):
      itr = iter(iterable)
      itr_finish = getattr(itr, "__finish__", None)
      if itr_finish is None:
          yield itr
      else:
          try:
              yield partial_iter(itr)
          finally:
              itr_finish()

그러면 이것을 다음과 같이 사용할 수 있습니다:

do finishing(finishable_itr) as itr:
    for header_item in itr:
        if end_of_header(header_item):
            break
        # process header item
    for body_item in itr:
        # process body item

종료 가능하지 않은 이터레이터에는 위의 어떤 것도 필요하지 않다는 점에 유의하십시오. __finish__() 메서드가 없으면 for 루프가 이터레이터를 신속하게 종료 처리하지 않으므로, 본질적으로 부분 순회가 가능합니다. 종료 가능하지 않은 이터레이터의 부분 순회를 기본 동작으로 허용하는 것은 이터레이터 프로토콜에 대한 이번 추가 사항의 하위 호환성을 유지하는 핵심 요소입니다.

감사의 글

이 글은 PEP 340에 대한 논의에서 발전해 나온 것이므로 해당 PEP의 감사의 글 섹션이 그대로 적용됩니다. 다만 애초에 PEP 310과 PEP 340을 작성한 Michael Hudson, Paul Moore, Guido van Rossum에게, 그리고 이 글에 반영된 특정 아이디어를 떠올리게 해 준 Fredrik Lundh, Phillip J. Eby, Steven Bethard, Josiah Carlson, Greg Ewing, Tim Delaney, Arnold deVos(순서는 특별한 의미가 없습니다)에게 추가로 감사를 드립니다.

참고 문헌