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

Python 개선 제안 한국어 번역

PEP 340 – 익명 블록 문

Author:
Guido van Rossum
Status:
Rejected
Type:
Standards Track
Created:
27-Apr-2005
Post-History:


Table of Contents

번역·라이선스 안내

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

서론

이 PEP는 자원 관리 목적으로 사용할 수 있는 새로운 유형의 복합 문을 제안합니다. 사용할 키워드가 아직 선택되지 않았기 때문에 새로운 문 유형을 잠정적으로 블록 문이라고 부릅니다.

이 PEP는 여러 다른 PEP와 경쟁합니다: PEP 288 (제너레이터 속성과 예외; 두 번째 부분만 해당), PEP 310 (신뢰할 수 있는 획득/해제 쌍), 그리고 PEP 325 (제너레이터를 위한 자원 해제 지원)입니다.

제너레이터를 사용하여 블록 문을 “구동”하는 것은 실제로 분리 가능한 제안이라는 점을 분명히 해야 합니다. PEP의 블록 문 정의만으로도 클래스를 사용하여 모든 예제를 구현할 수 있습니다(쉽게 템플릿으로 바꿀 수 있는 예제 6과 유사합니다). 그러나 핵심 아이디어는 제너레이터를 사용하여 블록 문을 구동하는 것입니다. 나머지는 상세한 설명이므로 이 두 부분을 함께 유지하고 싶습니다.

(PEP 342 향상된 이터레이터는 원래 이 PEP의 일부였지만, 두 제안은 실제로 독립적이며 Steven Bethard의 도움으로 이를 별도의 PEP로 옮겼습니다.)

거부 공지

이 PEP를 PEP 343을 지지하며 거부합니다. 이 거부의 근거는 해당 PEP의 동기 부여 절을 참조하십시오. GvR.

동기와 요약

(Shane Hathaway에게 감사드립니다 – 안녕하세요, Shane!)

훌륭한 프로그래머는 흔히 사용하는 코드를 재사용 가능한 함수로 옮깁니다. 그러나 때로는 실제 문의 순서가 아니라 함수 구조에서 패턴이 나타납니다. 예를 들어 많은 함수가 잠금을 획득하고, 해당 함수에 특화된 코드를 실행한 다음, 조건 없이 잠금을 해제합니다. 이를 사용하는 모든 함수에서 잠금 코드를 반복하면 오류가 발생하기 쉽고 리팩터링이 어려워집니다.

블록 문은 구조 패턴을 캡슐화하는 메커니즘을 제공합니다. 블록 문 내부의 코드는 블록 이터레이터라고 하는 객체의 제어 아래 실행됩니다. 단순한 블록 이터레이터는 블록 문 내부의 코드 전후에 코드를 실행합니다. 블록 이터레이터는 제어하는 코드를 두 번 이상 실행하거나(또는 전혀 실행하지 않거나), 예외를 포착하거나, 블록 문의 본문에서 데이터를 받을 수도 있습니다.

블록 이터레이터를 작성하는 편리한 방법은 제너레이터를 작성하는 것입니다(PEP 255). 제너레이터는 Python 함수와 매우 비슷해 보이지만, 값을 즉시 반환하는 대신 “yield” 문에서 실행을 일시 중지합니다. 제너레이터를 블록 이터레이터로 사용하면 yield 문은 Python 인터프리터에 블록 이터레이터를 일시 중지하고, 블록 문의 본문을 실행한 다음, 본문이 실행을 마치면 블록 이터레이터를 재개하도록 지시합니다.

Python 인터프리터는 제너레이터를 기반으로 하는 블록 문을 만났을 때 다음과 같이 동작합니다. 먼저 인터프리터는 제너레이터의 인스턴스를 만들고 실행을 시작합니다. 제너레이터는 캡슐화하는 패턴에 적합한 설정 작업을 수행합니다. 예를 들면 잠금 획득, 파일 열기, 데이터베이스 트랜잭션 시작 또는 루프 시작 등이 있습니다. 그런 다음 제너레이터는 yield 문을 사용하여 블록 문의 본문에 실행을 넘깁니다. 블록 문의 본문이 완료되거나, 포착되지 않은 예외를 발생시키거나, continue 문을 사용하여 제너레이터에 데이터를 되돌려 보내면 제너레이터가 재개됩니다. 이 시점에서 제너레이터는 정리한 후 중지하거나 다시 yield할 수 있으며, 이 경우 블록 문의 본문이 다시 실행됩니다. 제너레이터가 종료되면 인터프리터는 블록 문을 빠져나옵니다.

사용 사례

끝부분의 Examples 섹션을 참조하십시오.

사양: __exit__() 메서드

이터레이터를 위한 선택적 새 메서드인 __exit__()를 제안합니다. 이 메서드는 raise 문에 대한 세 가지 “인자”인 type, value, traceback에 각각 대응하는 최대 세 개의 인자를 받습니다. 세 인자가 모두 None이면, 적절한 기본값을 제공하기 위해 sys.exc_info()를 참조할 수 있습니다.

사양: 익명 블록 문

다음 구문을 사용하는 새 문을 제안합니다.:

block EXPR1 as VAR1:
    BLOCK1

여기서 ‘block’과 ‘as’는 새 키워드이며, EXPR1은 임의의 표현식(단, 표현식 목록은 아님)이고 VAR1은 임의의 대입 대상(쉼표로 구분된 목록일 수 있음)입니다.

“as VAR1” 부분은 선택 사항입니다. 생략하면 아래 번역에서 VAR1에 대한 할당은 생략되지만(할당되는 표현식은 계속 평가됩니다!),

‘block’ 키워드의 선택은 논쟁의 여지가 있습니다. 전혀 키워드를 사용하지 않는 것을 포함하여 많은 대안이 제안되었으며, 저는 실제로 그 방안을 선호합니다. PEP 310은 유사한 의미론에 ‘with’를 사용하지만, 저는 이를 Pascal과 VB에서 볼 수 있는 것과 유사한 with 문을 위해 남겨 두고 싶습니다. (다만 C# 설계자들이 ‘with’를 좋아하지 않는다는 것을 [2]에서 방금 알게 되었고, 그들의 논리에 동의해야 합니다.) 이 문제를 잠시 피하기 위해, 적절한 키워드가 무엇인지, 또는 키워드가 필요한지에 합의할 때까지 ‘block’을 사용하겠습니다.

‘as’ 키워드는 논쟁의 여지가 없다는 점에 유의하십시오(마침내 정식 키워드 상태로 승격될 예정입니다).

블록 문이 여러 번 반복되는 루프를 나타내는지 여부는 이터레이터가 결정한다는 점에 유의하십시오. 가장 일반적인 사용 사례에서는 BLOCK1이 정확히 한 번 실행됩니다. 그러나 파서 관점에서는 항상 루프이므로, break와 continue의 제어는 블록의 이터레이터로 전달됩니다(자세한 내용은 아래를 참조하십시오).

변환은 for 루프와 미묘하게 다릅니다. iter()는 호출되지 않으므로 EXPR1은 이미 이터레이터여야 하며(단순히 이터러블이어서는 안 됨), break, return 또는 예외로 인해 블록 문을 벗어나는지 여부와 관계없이 이터레이터에 반드시 알립니다.:

itr = EXPR1  # The iterator
ret = False  # True if a return statement is active
val = None   # Return value, if ret == True
exc = None   # sys.exc_info() tuple if an exception is active
while True:
    try:
        if exc:
            ext = getattr(itr, "__exit__", None)
            if ext is not None:
                VAR1 = ext(*exc)   # May re-raise *exc
            else:
                raise exc[0], exc[1], exc[2]
        else:
            VAR1 = itr.next()  # May raise StopIteration
    except StopIteration:
        if ret:
            return val
        break
    try:
        ret = False
        val = exc = None
        BLOCK1
    except:
        exc = sys.exc_info()

(그러나 ‘itr’ 등의 변수는 사용자가 볼 수 없으며, 사용되는 내장 이름은 사용자가 재정의할 수 없습니다.)

BLOCK1내부에는 다음과 같은 특별한 변환이 적용됩니다.

  • “break”는 항상 유효하며 다음과 같이 변환됩니다.:
    exc = (StopIteration, None, None)
    continue
    
  • “return EXPR3”는 블록 문이 함수 정의 안에 포함된 경우에만 유효하며 다음과 같이 변환됩니다.:
    exc = (StopIteration, None, None)
    ret = True
    val = EXPR3
    continue
    

결과적으로 break와 return은 블록 문이 for 루프인 경우와 거의 동일하게 동작합니다. 다만 선택적 __exit__() 메서드를 통해 블록 문을 벗어나기 전에 이터레이터가 리소스를 정리할 기회를 얻습니다. 블록 문을 예외 발생을 통해 벗어나는 경우에도 이터레이터에 기회가 주어집니다. 이터레이터에 __exit__() 메서드가 없으면 for 루프와 차이가 없습니다(단, for 루프는 EXPR1에 대해 iter()를 호출합니다).

블록 문의 yield 문은 다르게 처리되지 않는다는 점에 유의하십시오. 블록의 이터레이터에 알리지 않고 블록을 포함하는 함수를 일시 중단합니다. 로컬 제어 흐름이 실제로 블록을 벗어나지 않으므로 블록의 이터레이터는 이 yield를 전혀 인식하지 못합니다. 다시 말해, 이는 break 또는 return 문과 같지 않습니다. yield로 재개된 루프가 next()를 호출하면 블록은 yield 직후부터 재개됩니다. (아래 예제 7을 참조하십시오.) 아래에 설명된 제너레이터 최종화 의미론은 (모든 최종화 의미론의 한계 내에서) 블록이 결국 재개되도록 보장합니다.

for 루프와 달리 블록 문에는 else 절이 없습니다. 이는 혼란을 일으키고 블록 문의 “루프스러움”을 강조할 것이라고 생각합니다. 반면 저는 블록 문이 for 루프와 다르다는 점을 강조하고 싶습니다. 또한 else 절에는 가능한 의미론이 여러 가지이며, 사용 사례도 매우 제한적입니다.

사양: 제너레이터 종료 처리

제너레이터는 새로운 __exit__() 메서드 API를 구현합니다.

제너레이터는 try-finally 문 내부에 yield 문을 포함할 수 있게 됩니다.

yield 문에 대한 표현식 인자는 선택 사항이 되며, 기본값은 None입니다.

__exit__()가 호출되면 제너레이터가 재개되지만, yield 문이 있는 지점에서 __exit__ 인자로 표현된 예외가 발생합니다. 제너레이터는 이 예외를 다시 발생시키거나, 다른 예외를 발생시키거나, 또 다른 값을 yield할 수 있습니다. 단, __exit__()에 전달된 예외가 StopIteration인 경우에는 StopIteration을 발생시켜야 합니다. 그렇지 않으면 break가 continue로 바뀌는 결과가 되며, 적어도 이는 예상 밖의 동작입니다. 제너레이터를 재개하는 최초 호출이 next() 호출이 아니라 __exit__() 호출인 경우, 제너레이터의 실행이 중단되고 제너레이터 본문에 제어를 전달하지 않은 채 예외가 다시 발생합니다.

아직 종료되지 않은 제너레이터가 (참조 카운팅이나 순환 가비지 수집기를 통해) 가비지 수집되면, 해당 제너레이터의 __exit__() 메서드가 첫 번째 인자로 StopIteration을 받아 한 번 호출됩니다. 제너레이터가 StopIteration을 인자로 하여 __exit__()가 호출될 때 StopIteration을 발생시켜야 한다는 요구 사항과 함께, 이는 제너레이터가 마지막으로 일시 중지되었을 때 활성 상태였던 모든 finally 절이 결국 실행되도록 보장합니다. 물론 특정 상황에서는 제너레이터가 가비지 수집되지 않을 수도 있습니다. 이는 다른 객체의 파이널라이저(__del__() 메서드)에 대해 제공되는 보장과 다르지 않습니다.

고려했으나 거부한 대안

  • ‘block’에 대한 많은 대안이 제안되었습니다. 지금까지 ‘block’보다 더 마음에 드는 다른 키워드 제안은 보지 못했습니다. 아쉽게도 ‘block’ 역시 좋은 선택은 아닙니다. 변수, 인자 및 메서드에 상당히 많이 사용되는 이름이기 때문입니다. 결국 ‘with’가 가장 좋은 선택일지도 모릅니다.
  • 이상적인 키워드를 고르려고 하는 대신, block 문은 단순히 다음과 같은 형태를 가질 수 있습니다.:
    EXPR1 as VAR1:
        BLOCK1
    

    이는 처음에는 매력적으로 보입니다. EXPR1에 사용되는 함수 이름을 (아래 Examples 절에 있는 이름처럼) 잘 선택하면 읽기 좋고 “사용자 정의 문”처럼 느껴지기 때문입니다. 그러나 이는 저를 비롯한 많은 사람을 불편하게 합니다. 키워드가 없으면 구문이 매우 “밋밋하고”, 설명서에서 찾아보기도 어려우며(‘as’가 선택 사항이라는 점을 기억하십시오), block 문에서 break와 continue의 의미를 더욱 혼란스럽게 만듭니다.

  • Phillip Eby는 둘을 구별하기 위해 block 문이 for 루프와는 완전히 다른 API를 사용하도록 하자고 제안했습니다. 제너레이터가 block API를 지원하도록 하려면 제너레이터를 데코레이터로 감싸야 합니다. 제 생각에는 이는 이점은 거의 없으면서 복잡성만 더합니다. 또한 block 문이 개념적으로 루프라는 점을 실제로 부정할 수도 없습니다. 결국 break와 continue를 지원하기 때문입니다.
  • 다음과 같은 제안이 계속해서 나옵니다. 즉, “block EXPR1 as VAR1” 대신 “block VAR1 = EXPR1”로 하자는 것입니다. 이는 매우 오해의 소지가 있습니다. VAR1에는 EXPR1의 값이 할당되지 않기 때문입니다. EXPR1은 내부 변수에 할당되는 제너레이터를 생성하고, VAR1은 해당 이터레이터의 __next__() 메서드를 연속해서 호출할 때 반환되는 값입니다.
  • 변환을 iter(EXPR1)를 적용하도록 변경하면 어떻습니까? 모든 예제가 계속 작동합니다. 그러나 이렇게 하면 block 문이 for 루프와 비슷해지는 반면, 강조점은 둘의 차이에 있어야 합니다. iter()를 호출하지 않으면 시퀀스를 EXPR1로 사용하는 것과 같은 여러 오해를 잡아낼 수 있습니다.

Thunk와의 비교

block 문에 대해 제안된 대안적 의미론은 block을 thunk(포함하는 스코프에 자연스럽게 섞이는 익명 함수)로 바꿉니다.

제가 볼 수 있는 thunk의 주요 장점은 버튼 위젯의 콜백처럼 thunk를 나중에 사용할 수 있도록 저장할 수 있다는 점입니다(이렇게 하면 thunk는 클로저가 됩니다). yield 기반 block에는 이를 사용할 수 없습니다(단, thunk 기반 구현과 함께 yield 구문을 사용하는 Ruby에서는 가능합니다). 그러나 저는 이것을 거의 장점으로 봐야 한다고 말할 수 있습니다. block을 보고 그것이 정상적인 제어 흐름에서 실행될지 나중에 실행될지 알 수 없다면 약간 불편할 것이라고 생각합니다. 그 목적을 위해 명시적인 중첩 함수를 정의하는 경우에는 저에게 이런 문제가 없습니다. ‘def’ 키워드가 함수 본문이 나중에 실행된다는 의미라는 것을 이미 알고 있기 때문입니다.

썽크를 그것이 실제로 그러한 익명 함수라고 생각하면, 썽크 안의 return 문은 포함하는 함수가 아니라 썽크에서 반환한다고 말할 수밖에 없습니다. 다른 방식으로 처리하면 썽크가 클로저로서 자신을 포함하는 함수보다 오래 존속할 때 상당히 이상한 일이 발생합니다(연속 전달이 도움이 될 수도 있지만, 저는 거기까지 다루지는 않겠습니다 :-).

그러나 그러면 리소스 정리 템플릿 패턴의 중요하다고 생각하는 사용 사례를 잃게 됩니다. 저는 보통 다음과 같은 코드를 작성합니다.:

def findSomething(self, key, default=None):
    self.lock.acquire()
    try:
         for item in self.elements:
             if item.matches(key):
                 return item
         return default
    finally:
       self.lock.release()

그리고 이것을 다음과 같이 작성할 수 없다면 아쉬울 것입니다.:

def findSomething(self, key, default=None):
    block locking(self.lock):
         for item in self.elements:
             if item.matches(key):
                 return item
         return default

이 특정 예제는 break를 사용하여 다시 작성할 수 있습니다.:

def findSomething(self, key, default=None):
    block locking(self.lock):
         for item in self.elements:
             if item.matches(key):
                 break
         else:
             item = default
     return item

그러나 억지스러워 보이고 변환이 항상 그렇게 쉽지는 않습니다. 코드를 단일 반환 방식으로 다시 작성해야 하므로 지나치게 제한적인 느낌이 듭니다.

또한 썽크 안의 yield가 갖는 의미론적 난점에도 주목해야 합니다. 합리적으로 해석할 수 있는 유일한 방법은 이것이 썽크를 제너레이터로 바꾼다고 보는 것입니다!

Greg Ewing은 썽크가 “예외와 break/continue/return 문을 이리저리 조작하지 않고 필요한 작업만 수행하므로 훨씬 더 단순해질 것이라고 생각합니다. 그것이 무엇을 하고 왜 유용한지 설명하기도 쉬울 것입니다.”라고 믿습니다.

그러나 썽크와 이를 포함하는 함수 사이에 필요한 지역 변수 공유를 얻으려면, 썽크에서 사용되거나 설정되는 모든 지역 변수가 ‘셀’이 되어야 합니다(중첩된 스코프 사이에서 변수를 공유하기 위한 메커니즘입니다). 셀은 일반 지역 변수에 비해 접근 속도를 늦춥니다. 접근할 때 추가적인 C 함수 호출(PyCell_Get() 또는 PyCell_Set())이 필요하기 때문입니다.

아마도 전혀 우연이 아니겠지만, 위의 마지막 예제(블록 내부의 return을 피하도록 다시 작성한 findSomething())는 일반적인 중첩 함수와 달리 썽크가 할당하는 변수도 이를 포함하는 함수와 공유해야 하며, 해당 변수가 썽크 외부에서는 할당되지 않더라도 그렇다는 점을 보여 줍니다.

Greg Ewing은 다시 다음과 같이 말합니다. “제너레이터가 더 강력한 것으로 밝혀졌습니다. 한 번에 둘 이상을 실행할 수 있기 때문입니다. 여기서 그 기능을 사용할 일이 있을까요?”

저는 이 기능에 분명히 용도가 있다고 생각합니다. 이미 여러 사람이 제너레이터를 사용하여 비동기 경량 스레드를 구현하는 방법을 보여 주었습니다(예를 들어 PEP 288에서 인용된 David Mertz와 Fredrik Lundh [3]).

마지막으로 Greg은 다음과 같이 말합니다. “적절한 문법을 고안할 수 있다면 썽크 구현은 여러 블록 인자를 쉽게 처리할 가능성이 있습니다. 제너레이터 구현으로 이를 일반적인 방식으로 처리할 수 있을지는 알기 어렵습니다.”

그러나 여러 블록에 대한 사용 사례는 찾기 어려워 보입니다.

(그 후 이러한 반론의 대부분을 제거하도록 썽크 구현을 변경하자는 제안이 나왔지만, 그 결과의 의미론은 설명하고 구현하기에 상당히 복잡하므로, 제 생각에는 이는 애초에 썽크를 사용하는 목적을 무너뜨립니다.)

예제

(이러한 예제 중 여러 개에는 “yield None”이 포함되어 있습니다. 관련 PEP 342가 채택되면, 물론 이것들은 단순히 “yield”로 변경할 수 있습니다.)

  1. 블록 시작 시 획득한 잠금이 블록을 벗어날 때 해제되도록 보장하는 템플릿입니다.:
    def locking(lock):
        lock.acquire()
        try:
            yield None
        finally:
            lock.release()
    

    다음과 같이 사용합니다.:

    block locking(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. 블록을 벗어날 때 파일이 닫히도록 보장하는 파일 열기 템플릿입니다.:
    def opening(filename, mode="r"):
        f = open(filename, mode)
        try:
            yield f
        finally:
            f.close()
    

    다음과 같이 사용합니다.:

    block opening("/etc/passwd") as f:
        for line in f:
            print line.rstrip()
    
  3. 데이터베이스 트랜잭션을 커밋하거나 롤백하기 위한 템플릿입니다.:
    def transactional(db):
        try:
            yield None
        except:
            db.rollback()
            raise
        else:
            db.commit()
    
  4. 어떤 작업을 최대 n번까지 시도하는 템플릿입니다.:
    def auto_retry(n=3, exc=Exception):
        for i in range(n):
            try:
                yield None
                return
            except exc, err:
                # perhaps log exception here
                continue
        raise # re-raise the exception we caught earlier
    

    다음과 같이 사용합니다.:

    block auto_retry(3, IOError):
        f = urllib.urlopen("https://www.example.com/")
        print f.read()
    
  5. 블록을 중첩하고 템플릿을 결합할 수 있습니다.:
    def locking_opening(lock, filename, mode="r"):
        block locking(lock):
            block opening(filename) as f:
                yield f
    

    다음과 같이 사용합니다.:

    block locking_opening(myLock, "/etc/passwd") as f:
        for line in f:
            print line.rstrip()
    

    (이 예제가 헷갈린다면, 이것이 다른 이터레이터나 제너레이터를 재귀적으로 호출하는 일반 제너레이터에서 본문에 yield가 있는 for 루프를 사용하는 것과 동등하다는 점을 고려하십시오. 예를 들어 os.walk()의 소스 코드를 참조하십시오.)

  6. 예제 1의 시맨틱스를 갖는 일반 이터레이터를 작성하는 것도 가능합니다:
    class locking:
       def __init__(self, lock):
           self.lock = lock
           self.state = 0
       def __next__(self, arg=None):
           # ignores arg
           if self.state:
               assert self.state == 1
               self.lock.release()
               self.state += 1
               raise StopIteration
           else:
               self.lock.acquire()
               self.state += 1
               return None
       def __exit__(self, type, value=None, traceback=None):
           assert self.state in (0, 1, 2)
           if self.state == 1:
               self.lock.release()
           raise type, value, traceback
    

    (이 예제는 다른 예제들을 구현하도록 쉽게 수정할 수 있으며, 같은 목적에 대해 제너레이터가 얼마나 더 단순한지 보여줍니다.)

  7. stdout을 일시적으로 리디렉션하기:
    def redirecting_stdout(new_stdout):
        save_stdout = sys.stdout
        try:
            sys.stdout = new_stdout
            yield None
        finally:
            sys.stdout = save_stdout
    

    다음과 같이 사용합니다:

    block opening(filename, "w") as f:
        block redirecting_stdout(f):
            print "Hello world"
    
  8. 오류 조건도 반환하는 opening()의 변형입니다:
    def opening_w_error(filename, mode="r"):
        try:
            f = open(filename, mode)
        except IOError, err:
            yield None, err
        else:
            try:
                yield f, None
            finally:
                f.close()
    

    다음과 같이 사용합니다:

    block opening_w_error("/etc/passwd", "a") as f, err:
        if err:
            print "IOError:", err
        else:
            f.write("guido::0:0::/:/bin/sh\n")
    

감사의 말

특별한 순서 없이: Alex Martelli, Barry Warsaw, Bob Ippolito, Brett Cannon, Brian Sabbey, Chris Ryland, Doug Landauer, Duncan Booth, Fredrik Lundh, Greg Ewing, Holger Krekel, Jason Diamond, Jim Jewett, Josiah Carlson, Ka-Ping Yee, Michael Chermside, Michael Hudson, Neil Schemenauer, Alyssa Coghlan, Paul Moore, Phillip Eby, Raymond Hettinger, Georg Brandl, Samuele Pedroni, Shannon Behrens, Skip Montanaro, Steven Bethard, Terry Reedy, Tim Delaney, Aahz, 그리고 그 외의 여러분에게 감사드립니다. 소중한 기여를 해 주신 모든 분들께 감사드립니다!

참고 문헌

[1] https://mail.python.org/pipermail/python-dev/2005-April/052821.html