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의 “신뢰할 수 있는 획득/해제 쌍”과 Guido의 PEP 340의 “익명 블록 문”을 결합한 것입니다. 이 PEP는 PEP 340의 장점을 취하고 PEP 310의 일부와 결합한 뒤, 이를 모두 우아한 전체로 재구성하는 것을 목표로 합니다. 완전한 그림을 제시하기 위해 다양한 다른 PEP에서 아이디어를 차용하며, 독립적으로 성립하도록 작성되었습니다.

작성자의 주

관련 PEP 340 논의 중에 저는 이 PEP의 초안을 PEP 3XX라는 이름으로 제 웹사이트에서 관리했습니다(제출된 PEP를 python-dev의 활동을 추적할 만큼 빠르게 업데이트할 수 있는 CVS 접근 권한이 없었기 때문입니다).

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

따라서 python.org에 보관을 위해 제출된 이 PEP의 버전은 제출 직후 철회됩니다. PEP 343과 통합된 제너레이터 개선 PEP가 중요한 아이디어를 다루게 됩니다.

서론

이 PEP는 임의의 try/finally 및 일부 try/except/else 상용구를 분리할 수 있는 새로운 with문을 도입하여 Python의 안정적인 리소스 관리 능력을 향상할 것을 제안합니다. 이 새로운 구문을 ‘사용자 정의 문’이라고 하며, 이와 연관된 클래스 정의를 ‘문장 템플릿’이라고 합니다.

위 내용이 이 PEP의 핵심입니다. 그러나 그것이 전부였다면 PEP 310만으로 충분했을 것이며, 이 PEP는 본질적으로 중복되었을 것입니다. 대신 이 PEP는 적절히 데코레이터가 적용된 제너레이터를 사용하여 이러한 문장 템플릿을 자연스럽게 작성할 수 있도록 하는 추가 개선 사항을 권장합니다. 이러한 개선 사항의 부작용으로 제너레이터 내부의 리소스 관리를 적절히 처리하는 것이 중요해집니다.

이는 PEP 343와 매우 유사하지만, 발생한 예외가 제너레이터 프레임 내부에서 다시 발생하며, 그 결과 제너레이터 종료 문제를 해결해야 합니다. 이 PEP에서 제안하는 템플릿 제너레이터 데코레이터는 PEP 340의 일회용 템플릿과 달리 재사용 가능한 템플릿도 생성합니다.

관련 PEP 340과 비교하면, 이 PEP는 예외를 억제하는 기능을 제거하고 사용자 정의 문을 루프가 아닌 구문으로 만듭니다. 또 다른 주요 차이점은 제너레이터를 문장 템플릿으로 변환하기 위해 데코레이터를 사용하고, 이터레이터 종료 문제를 해결하기 위한 아이디어를 통합한다는 점입니다.

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

다른 PEP와의 관계

이 PEP는 PEP 310, PEP 340PEP 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 루프의 반복 변수와 마찬가지로 VAR1BLOCK1과 사용자 정의 문장 뒤의 코드 모두에서 보입니다.

문장 템플릿은 예외에 반응만 할 수 있으며, 예외를 억제할 수는 없다는 점에 유의하십시오. 그 이유에 대한 설명은 Rejected Options에서 확인하십시오.

문장 템플릿 프로토콜: __enter__

__enter__() 메서드는 인자를 받지 않으며, 이 메서드가 예외를 발생시키면 BLOCK1은 절대로 실행되지 않습니다. 이 경우 __exit__() 메서드는 호출되지 않습니다. 이 메서드가 반환한 값은 as 절이 사용되는 경우 VAR1에 할당됩니다. 반환할 다른 값이 없는 객체는 일반적으로 None대신 self를 반환해야 with 문에서 제자리 생성을 사용할 수 있습니다.

문장 템플릿은 이 메서드를 사용하여 문장 실행 중에 존재해야 하는 조건(예: 동기화 잠금 획득)을 설정해야 합니다.

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

문장 템플릿 프로토콜: __exit__

__exit__() 메서드는 raise 문에 대한 세 가지 “인자”인 type, value, traceback에 대응하는 세 인자를 받습니다. 인자는 항상 모두 제공되며, 예외가 발생하지 않은 경우 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_BLOCKbreak, return 또는 continue 문이 나오면 문이 완료될 때 FINALLY_BLOCK만 실행됩니다. 문장 템플릿에서는 이러한 문이 다른 비예외 블록 종료와 동일하게 취급되므로 ELSE_BLOCK도 실행됩니다. 이러한 사항이 중요한 사용 사례에서는 이것이 좋은 동작일 가능성이 높습니다(transaction을 Examples_에서 참조하십시오). 예외 처리기를 작성할 때 except 절과 else 절 중 어느 것도 실행되지 않는 이 빈틈을 쉽게 잊기 때문입니다.

둘째, 문장 템플릿은 어떤 예외도 억제하지 않습니다. 예를 들어 원래 코드가 exc_type1exc_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는 단계적 구현의 명백한 후보가 됩니다(with 문은 1단계에서, 제너레이터 통합은 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가 승인되면 yieldreturn과 마찬가지로 기본값 None을 제공합니다(즉, yieldyield 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__() 메서드

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

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

템플릿 제너레이터 래퍼에는 예외가 전달되지 않으면 제너레이터에서 next()를 단순히 호출하는 __exit__() 메서드가 있습니다. 예외가 전달되면 마지막 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

(Examples_에서 all_lines의 정의와 즉시 종료가 필요한 이유를 참조하십시오.)

위 예제를 파일 객체의 사용 방식과 비교하십시오.:

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

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

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

예외에 대응하거나 리소스를 정리하려면 적절하게 구성한 try 문으로 yield문을 감싸기만 하면 됩니다. 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. 표준 출력을 일시적으로 리디렉션합니다:
    @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 340block 명령문에서 볼 수 있듯이, 루프 내부에 있고 breakcontinue 명령문을 포함하는 try 명령문을 분리할 때 문제가 발생한다는 점입니다(이러한 명령문이 원래 루프가 아니라 block 구성에 적용되기 때문입니다). 억제를 제외한 임의의 예외 처리를 문 템플릿으로 추출할 수 있게 하는 것이 핵심 목표이므로, 이는 명백한 문제입니다.

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

마지막으로, 반복 구문을 사용하면 TOOWTDI와 관련하여 중대한 문제가 발생합니다. 특정 상황을 일반적인 for 루프로 처리해야 하는지 새로운 반복 구문으로 처리해야 하는지 불분명한 경우가 많기 때문입니다. 현재 PEP에서는 이러한 문제가 없습니다. 반복에는 계속 for 루프를 사용하고, 예외 처리를 추출하는 데는 새로운 do 문을 사용합니다.

특히 PEP 340의 익명 블록 문과 관련된 또 다른 문제는 문 템플릿을 직접 작성하기가 매우 어려워진다는 점입니다(즉, 제너레이터를 사용하지 않고 작성하는 경우). 현재 제안에서는 이 문제를 해결하며, Examples 에 있는 다양한 클래스 기반 문 템플릿 구현이 상대적으로 단순하다는 점에서 이를 확인할 수 있습니다.

문 템플릿에서 예외를 억제하도록 허용하는 것

이 PEP의 초기 버전에서는 문 템플릿이 예외를 억제할 수 있었습니다. BDFL은 이와 관련된 복잡성에 우려를 표명했으며, 저는 Raymond Chen이 C 코드의 매크로 내부에 흐름 제어를 숨기는 것의 폐해에 관해 쓴 글 [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 내부에서 값을 생성하는 것을 금지하는 규칙이 유지되므로 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 사용하기

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

do가 처음 제안되었을 때 BDFL은 파스칼/델파이 스타일의 with 문에 사용하려는 의도에 따라 PEP 310with 키워드를 거부했습니다. 그 후 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

제안에서 사용된 구체적인 메서드 이름과 관련하여 사소한 점이 두 가지 더 있습니다. __try__() 메서드의 이름은 오해의 소지가 있습니다. SETUP_BLOCKtry 문에 진입하기 전에 실행되기 때문입니다. 또한 __else__() 메서드의 이름은 단독으로 보면 명확하지 않습니다. 다른 많은 Python 문에도 else 절이 포함되기 때문입니다.

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

제너레이터 내부에서 사용자 정의 문을 사용할 수 있는 기능은 자원 관리가 현재처럼 외부에서 처리되는 대신 제너레이터 내부로 이동함에 따라, 이터레이터의 결정적 종료 처리에 대한 필요성을 높일 가능성이 큽니다.

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

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

이터레이터를 위한 선택적 새 메서드로 __finish__()라는 메서드를 제안합니다. 이 메서드는 인자를 받지 않으며 아무것도 반환하지 않아야 합니다.

__finish__ 메서드는 이터레이터가 열어 둔 모든 자원을 정리할 것으로 예상됩니다. __finish__() 메서드를 가진 이터레이터를 이 PEP의 나머지 부분에서는 ‘종료 가능한 이터레이터’라고 부릅니다.

최선 노력 종료 처리

종료 가능한 이터레이터는 종료 처리를 수행하는 __del__ 메서드도 제공해야 합니다(예를 들어 __finish__() 메서드를 호출하는 방식으로 수행합니다). 이를 통해 이터레이터에 결정적 종료 처리가 적용되지 않는 경우에도 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의 논의에서 비롯되었으므로 PEP 340의 감사의 글 섹션이 적용되지만, 애초에 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에게도 이 글에 반영된 특정 아이디어들을 이끌어내 주신 데 대해 감사드립니다.

참고 자료