[Rejected] PEP 340 - Anonymous Block Statements
원문 링크: PEP 340 - Anonymous Block Statements
상태: Rejected 유형: Standards Track 작성일: 27-Apr-2005
PEP 340 – 익명 블록 문 (Anonymous Block Statements)
작성자: Guido van Rossum 상태: 거부됨 (Rejected) 유형: 표준 트랙 (Standards Track) 생성일: 2005년 4월 27일
개요 (Introduction)
이 PEP는 리소스 관리 목적으로 사용될 수 있는 새로운 유형의 복합 문(compound statement)을 제안합니다. 새 문 유형은 사용될 키워드가 아직 선택되지 않았기 때문에 잠정적으로 ‘블록 문(block-statement)’이라고 불립니다.
이 PEP는 여러 다른 PEP들과 경쟁했습니다: PEP 288 (제너레이터 속성 및 예외; 두 번째 부분만 해당), PEP 310 (신뢰할 수 있는 획득/해제 쌍), 그리고 PEP 325 (제너레이터를 위한 리소스 해제 지원).
블록 문을 “구동”하기 위해 제너레이터(generator)를 사용하는 것은 실제로 분리 가능한 제안임을 명확히 해야 합니다. 이 PEP의 블록 문 정의만으로도 클래스를 사용하여 모든 예제를 구현할 수 있습니다 (예제 6과 유사하며 쉽게 템플릿으로 변환될 수 있습니다). 하지만 핵심 아이디어는 제너레이터를 사용하여 블록 문을 구동하는 것이며, 나머지는 상세 설명이므로 이 두 부분을 함께 유지하고 싶습니다.
(PEP 342, Enhanced Iterators는 원래 이 PEP의 일부였지만, 두 제안은 실제로 독립적이며 Steven Bethard의 도움으로 별도의 PEP로 옮겨졌습니다.)
거부 공지 (Rejection Notice)
이 PEP는 PEP 343을 지지하여 거부되었습니다. 이 거부 이유에 대한 설명은 해당 PEP의 동기 부여 섹션을 참조하십시오. - GvR.
동기 및 요약 (Motivation and Summary)
(Shane Hathaway에게 감사드립니다 – Hi Shane!)
훌륭한 프로그래머는 일반적으로 사용되는 코드를 재사용 가능한 함수로 옮깁니다. 그러나 때로는 실제 문(statement) 시퀀스보다는 함수의 구조에서 패턴이 나타나기도 합니다. 예를 들어, 많은 함수는 록(lock)을 획득하고, 해당 함수에 특정한 일부 코드를 실행한 다음, 무조건적으로 록을 해제합니다. 록을 사용하는 모든 함수에서 록 코드(locking code)를 반복하는 것은 오류를 발생시키기 쉽고 리팩토링(refactoring)을 어렵게 만듭니다.
블록 문은 구조의 패턴을 캡슐화하기 위한 메커니즘을 제공합니다. 블록 문 내부의 코드는 ‘블록 이터레이터(block iterator)’라고 불리는 객체의 제어하에 실행됩니다. 간단한 블록 이터레이터는 블록 문 내부의 코드 실행 전후에 코드를 실행합니다. 또한 블록 이터레이터는 제어되는 코드를 한 번 이상 실행하거나 (또는 전혀 실행하지 않거나), 예외를 catch하거나, 블록 문 본문으로부터 데이터를 수신할 수 있습니다.
블록 이터레이터를 작성하는 편리한 방법은 제너레이터(PEP 255)를 작성하는 것입니다. 제너레이터는 Python 함수와 매우 유사하게 보이지만, 값을 즉시 반환하는 대신 yield
문에서 실행을 일시 중지합니다. 제너레이터가 블록 이터레이터로 사용될 때, yield
문은 Python 인터프리터에게 블록 이터레이터를 일시 중지하고, 블록 문 본문을 실행한 다음, 본문이 실행되면 블록 이터레이터를 재개하도록 지시합니다.
Python 인터프리터는 제너레이터 기반의 블록 문을 만날 때 다음과 같이 동작합니다.
- 먼저, 인터프리터는 제너레이터를 인스턴스화하고 실행을 시작합니다.
- 제너레이터는 록 획득, 파일 열기, 데이터베이스 트랜잭션 시작, 루프 시작과 같이 캡슐화하는 패턴에 적합한 설정 작업을 수행합니다.
- 그런 다음 제너레이터는
yield
문을 사용하여 블록 문의 본문으로 실행을 넘겨줍니다. - 블록 문 본문이 완료되거나, 포착되지 않은 예외를 발생시키거나,
continue
문을 사용하여 제너레이터로 데이터를 다시 보내면 제너레이터가 재개됩니다. - 이 시점에서 제너레이터는 정리하고 중지하거나, 다시
yield
하여 블록 문 본문이 다시 실행되도록 할 수 있습니다. - 제너레이터가 완료되면 인터프리터는 블록 문을 떠납니다.
사용 사례 (Use Cases)
끝 부분의 예제 섹션을 참조하십시오.
사양: __exit__()
메서드 (Specification: the __exit__()
Method)
이터레이터(iterator)를 위한 선택적 새 메서드인 __exit__()
가 제안됩니다. 이 메서드는 raise
문에 대한 세 가지 “인수”(type, value, traceback)에 해당하는 최대 세 가지 인수를 받습니다. 세 인수가 모두 None
인 경우, sys.exc_info()
를 참조하여 적절한 기본값을 제공할 수 있습니다.
사양: 익명 블록 문 (Specification: the Anonymous Block Statement)
다음과 같은 구문을 가진 새로운 문이 제안됩니다:
block EXPR1 as VAR1:
BLOCK1
여기서 'block'
과 'as'
는 새로운 키워드입니다. EXPR1
은 임의의 표현식(expression)이고 (표현식 목록은 아님), VAR1
은 임의의 할당 대상(assignment target)입니다 (쉼표로 구분된 목록일 수 있습니다).
"as VAR1"
부분은 선택 사항입니다. 생략되면 아래 변환에서 VAR1
에 대한 할당은 생략됩니다 (하지만 할당되는 표현식은 여전히 평가됩니다!).
'block'
키워드의 선택은 논쟁의 여지가 많았습니다. 키워드를 전혀 사용하지 않는 것을 포함하여 (제가 실제로 좋아하는 방식) 많은 대안이 제안되었습니다. PEP 310은 유사한 의미를 위해 'with'
를 사용하지만, 저는 이 키워드를 Pascal과 VB에서 볼 수 있는 with
문과 유사한 것을 위해 남겨두고 싶습니다. (하지만 C# 디자이너들이 'with'
를 좋아하지 않는다는 것을 방금 알았고, 그들의 추론에 동의합니다.) 이 문제를 잠시 회피하기 위해 우리는 올바른 키워드에 동의할 수 있을 때까지 'block'
을 사용하고 있습니다.
'as'
키워드는 논쟁의 여지가 없습니다 (마침내 적절한 키워드 상태로 격상될 것입니다).
블록 문이 여러 번 반복되는 루프를 나타내는지 여부는 이터레이터가 결정합니다. 가장 일반적인 사용 사례에서는 BLOCK1
이 정확히 한 번 실행됩니다. 그러나 파서(parser)에게는 항상 루프입니다. break
와 continue
는 블록의 이터레이터로 제어를 전달합니다 (자세한 내용은 아래 참조).
이 변환은 for
루프와 미묘하게 다릅니다: iter()
는 호출되지 않으므로 EXPR1
은 이미 이터레이터여야 합니다 (단순한 이터러블(iterable)이 아님). 그리고 break
, return
또는 예외로 인해 블록 문이 종료되는지 여부와 관계없이 이터레이터는 블록 문이 종료될 때 알림을 받도록 보장됩니다.
itr = EXPR1 # 이터레이터
ret = False # return 문이 활성 상태이면 True
val = None # ret == True인 경우 반환 값
exc = None # 예외가 활성 상태이면 sys.exc_info() 튜플
while True:
try:
if exc:
ext = getattr(itr, "__exit__", None)
if ext is not None:
VAR1 = ext(*exc) # *exc를 다시 발생시킬 수 있음
else:
raise exc[0], exc[1], exc[2]
else:
VAR1 = itr.next() # 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 참조) 아래에 설명된 제너레이터 종료(finalization) 시맨틱은 (모든 종료 시맨틱의 한계 내에서) 블록이 결국 재개될 것임을 보장합니다.
for
루프와 달리 블록 문에는 else
절이 없습니다. 이는 혼란을 줄 수 있고 블록 문의 “루프성”을 강조할 수 있다고 생각하며, 저는 for
루프와의 차이점을 강조하고 싶습니다. 또한, else
절에 대한 여러 가능한 시맨틱이 있으며, 매우 약한 사용 사례만 있습니다.
사양: 제너레이터 종료 처리 (Specification: Generator Exit Handling)
제너레이터는 새로운 __exit__()
메서드 API를 구현합니다.
제너레이터는 try-finally
문 내에 yield
문을 가질 수 있습니다.
yield
문의 표현식 인수는 선택 사항이 됩니다 (기본값은 None
).
__exit__()
가 호출되면 제너레이터는 재개되지만, yield
문 지점에서 __exit__
인수로 표현된 예외가 발생합니다. 제너레이터는 이 예외를 다시 발생시키거나, 다른 예외를 발생시키거나, 다른 값을 yield
할 수 있습니다. 단, __exit__()
로 전달된 예외가 StopIteration
인 경우에는 StopIteration
을 발생시켜야 합니다 (그렇지 않으면 break
가 continue
로 바뀌는 예상치 못한 결과가 발생할 수 있습니다). 제너레이터를 재개하는 초기 호출이 next()
호출 대신 __exit__()
호출인 경우, 제너레이터의 실행은 중단되고 제어를 제너레이터 본문에 전달하지 않고 예외가 다시 발생됩니다.
아직 종료되지 않은 제너레이터가 가비지 컬렉션(참조 카운팅 또는 주기적 가비지 컬렉터에 의해)될 때, 첫 번째 인수로 StopIteration
을 사용하여 __exit__()
메서드가 한 번 호출됩니다. __exit__()
가 StopIteration
으로 호출될 때 제너레이터가 StopIteration
을 발생시켜야 한다는 요구 사항과 함께, 이는 제너레이터가 마지막으로 일시 중지되었을 때 활성화된 모든 finally
절이 결국 활성화되도록 보장합니다. 물론 특정 상황에서는 제너레이터가 결코 가비지 컬렉션되지 않을 수도 있습니다. 이는 다른 객체의 종료자 (__del__()
메서드)에 대해 보장되는 것과 다르지 않습니다.
고려되었으나 거부된 대안 (Alternatives Considered and Rejected)
'block'
에 대해 많은 대안이 제안되었습니다. 저는 아직 'block'
보다 더 마음에 드는 다른 키워드 제안을 보지 못했습니다. 안타깝게도 'block'
도 좋은 선택은 아닙니다. 변수, 인수 및 메서드에 대해 꽤 인기 있는 이름이기 때문입니다. 어쩌면 'with'
가 결국 최선의 선택일까요?
이상적인 키워드를 선택하려고 노력하는 대신, 블록 문은 단순히 다음 형식을 가질 수 있었습니다:
EXPR1 as VAR1:
BLOCK1
이것은 처음에는 매력적입니다. EXPR1
에 사용된 좋은 함수 이름 (아래 예제 섹션의 이름과 같은)과 함께 읽기 좋고 “사용자 정의 문(user-defined statement)”처럼 느껴지기 때문입니다. 하지만 이것은 저 (그리고 많은 다른 사람들)를 불편하게 만듭니다. 키워드가 없으면 구문이 매우 “밋밋하고”, 설명서에서 찾아보기 어렵고 ('as'
가 선택 사항임을 기억하십시오), 블록 문에서 break
와 continue
의 의미를 더욱 혼란스럽게 만듭니다.
Phillip Eby는 for
루프와 구별하기 위해 블록 문이 완전히 다른 API를 사용하도록 제안했습니다. 제너레이터는 블록 API를 지원하기 위해 데코레이터(decorator)로 래핑되어야 합니다. 제 생각에는 이것이 거의 이점 없이 더 많은 복잡성을 추가합니다. 그리고 블록 문이 개념적으로 루프라는 것을 부정할 수는 없습니다. 결국 break
와 continue
를 지원합니다.
이것은 계속 제안되고 있습니다: "block EXPR1 as VAR1"
대신 "block VAR1 = EXPR1"
. VAR1
에 EXPR1
의 값이 할당되지 않기 때문에 매우 오해의 소지가 있습니다. EXPR1
은 내부 변수에 할당되는 제너레이터를 생성하고, VAR1
은 해당 이터레이터의 __next__()
메서드에 대한 연속적인 호출에 의해 반환되는 값입니다.
변환이 iter(EXPR1)
를 적용하도록 변경하면 어떨까요? 모든 예제가 계속 작동할 것입니다. 그러나 이것은 블록 문을 for
루프와 더 유사하게 만들며, 둘 사이의 차이점을 강조해야 합니다. iter()
를 호출하지 않으면 EXPR1
로 시퀀스(sequence)를 사용하는 것과 같은 많은 오해를 막을 수 있습니다.
썽크(Thunks)와의 비교 (Comparison to Thunks)
블록 문에 대해 제안된 대안 시맨틱은 블록을 썽크 (포함하는 스코프(scope)에 녹아드는 익명 함수)로 바꿉니다.
제가 볼 수 있는 썽크의 주요 장점은 나중에 사용하기 위해 썽크를 저장할 수 있다는 것입니다. 예를 들어 버튼 위젯의 콜백(callback)처럼요 (썽크는 클로저(closure)가 됩니다). yield
기반 블록은 이런 용도로 사용할 수 없습니다 (Ruby에서는 썽크 기반 구현과 함께 yield
구문을 사용하지만 예외입니다). 하지만 저는 이것이 거의 장점이라고 생각합니다. 블록을 보고 그것이 일반적인 제어 흐름으로 실행될지 나중에 실행될지 모르는 것이 약간 불편할 것이기 때문입니다. 이 목적을 위해 명시적인 중첩 함수를 정의하는 것은 저에게 이런 문제를 일으키지 않습니다. 왜냐하면 저는 이미 'def'
키워드가 그 본문이 나중에 실행된다는 것을 의미한다는 것을 알고 있기 때문입니다.
썽크의 또 다른 문제는 우리가 썽크를 익명 함수로 생각하는 순간, 썽크 내의 return
문이 포함하는 함수가 아닌 썽크에서 반환한다고 말할 수밖에 없다는 것입니다. 다른 방식으로 하면 썽크가 클로저로 포함하는 함수를 넘어서 살아남을 때 큰 이상함을 초래할 것입니다 (아마도 continuations가 도움이 되겠지만, 저는 그쪽으로 가고 싶지 않습니다 :-).
그러나 그러면 리소스 정리 템플릿 패턴에 대한 중요한 사용 사례가 사라집니다. 저는 일반적으로 다음과 같은 코드를 작성합니다:
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
하지만 이것은 부자연스러워 보이고 변환이 항상 그렇게 쉽지는 않습니다. 코드를 단일 return
스타일로 다시 작성해야 할 것이며, 이는 너무 제한적이라고 느껴집니다.
또한 썽크 내의 yield
에 대한 의미론적 난제를 주목하십시오. 유일한 합리적인 해석은 이것이 썽크를 제너레이터로 바꾼다는 것입니다!
Greg Ewing은 썽크가 “예외와 break
/continue
/return
문을 가지고 장난칠 필요 없이 필요한 것만 수행하므로 훨씬 더 간단할 것입니다. 그것이 무엇을 하고 왜 유용한지 설명하기 쉬울 것입니다.”라고 믿습니다.
그러나 썽크와 포함하는 함수 사이에 필요한 로컬 변수 공유를 얻으려면, 썽크에서 사용되거나 설정된 모든 로컬 변수는 ‘셀(cell)’이 되어야 합니다 (중첩된 스코프 간에 변수를 공유하기 위한 우리의 메커니즘). 셀은 일반 로컬 변수에 비해 접근 속도를 늦춥니다. 접근에는 추가 C 함수 호출(PyCell_Get()
또는 PyCell_Set()
)이 포함됩니다.
어쩌면 완전히 우연은 아니지만, 위의 마지막 예제(findSomething()
을 블록 내 return
을 피하도록 다시 작성한 것)는 일반 중첩 함수와 달리 썽크에 의해 할당된 변수도 포함하는 함수와 공유되기를 원한다는 것을 보여줍니다. 썽크 외부에서 할당되지 않더라도 말이죠.
Greg Ewing이 다시 말합니다: “제너레이터는 한 번에 여러 개를 실행할 수 있기 때문에 더 강력하다는 것이 밝혀졌습니다. 이 기능이 여기에서 사용될 경우가 있을까요?”
저는 분명히 이것에 대한 사용 사례가 있다고 생각합니다. 여러 사람들이 이미 제너레이터를 사용하여 비동기 경량 스레드를 수행하는 방법을 보여주었습니다 (예: PEP 288에 인용된 David Mertz와 Fredrik Lundh).
그리고 마지막으로 Greg은 이렇게 말합니다: “썽크 구현은 적절한 구문이 고안될 수 있다면 여러 블록 인수를 쉽게 처리할 수 있는 잠재력이 있습니다. 제너레이터 구현으로 일반적인 방식으로 이것이 어떻게 수행될 수 있는지 보기 어렵습니다.”
그러나 여러 블록에 대한 사용 사례는 찾기 어렵습니다.
(그 이후로 썽크의 구현을 변경하여 이러한 대부분의 반론을 제거하려는 제안이 있었지만, 그 결과로 나타나는 시맨틱은 설명하고 구현하기에 상당히 복잡하므로, 제 생각에는 처음부터 썽크를 사용하는 목적을 무색하게 만듭니다.)
예제 (Examples)
(이 예제들 중 일부는 "yield None"
을 포함합니다. PEP 342가 승인되면, 이들은 물론 단순히 "yield"
로 변경될 수 있습니다.)
- 록 관리 템플릿: 블록 시작 시 획득된 록이 블록이 종료될 때 해제되도록 보장하는 템플릿입니다.
def locking(lock): lock.acquire() try: yield None finally: lock.release()
사용 예:
block locking(myLock): # 여기에 있는 코드는 myLock이 잡힌 상태에서 실행됩니다. # (return 또는 포착되지 않은 예외를 통해 종료되더라도) # 블록이 종료될 때 록이 해제되는 것이 보장됩니다.
- 파일 열기 템플릿: 파일이 블록을 떠날 때 닫히도록 보장하는 파일 열기 템플릿입니다.
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()
- 데이터베이스 트랜잭션 커밋 또는 롤백 템플릿:
def transactional(db): try: yield None except: db.rollback() raise else: db.commit()
- 최대 n회 재시도 템플릿:
def auto_retry(n=3, exc=Exception): for i in range(n): try: yield None return except exc, err: # 여기서 예외를 기록할 수 있습니다. continue raise # 이전에 catch한 예외를 다시 발생시킵니다.
사용 예:
block auto_retry(3, IOError): f = urllib.urlopen("https://www.example.com/") print f.read()
- 블록 중첩 및 템플릿 결합:
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()
의 소스 코드를 참조하십시오.) - 정규 이터레이터로 예제 1의 시맨틱 구현:
class locking: def __init__(self, lock): self.lock = lock self.state = 0 def __next__(self, arg=None): # 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
(이 예제는 다른 예제들을 구현하도록 쉽게 수정될 수 있습니다. 이는 동일한 목적을 위해 제너레이터가 얼마나 더 간단한지를 보여줍니다.)
- 표준 출력(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"
- 오류 조건을 반환하는
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")
감사의 글 (Acknowledgements)
유용성의 순서와 무관하게: 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, 그리고 기타 여러분. 소중한 기여에 감사드립니다!
참고 자료 (References)
https://mail.python.org/pipermail/python-dev/2005-April/052821.html https://web.archive.org/web/20060719195933/http://msdn.microsoft.com/vcsharp/programming/language/ask/withstatement/ https://web.archive.org/web/20050204062901/http://effbot.org/zone/asyncore-generators.htm
저작권 (Copyright)
이 문서는 공개 도메인에 배치되었습니다.## PEP 340 – 익명 블록 문 (Anonymous Block Statements)
작성자: Guido van Rossum 상태: 거부됨 (Rejected) 유형: 표준 트랙 (Standards Track) 생성일: 2005년 4월 27일
개요 (Introduction)
이 Python Enhancement Proposal (PEP)은 리소스 관리 목적으로 사용될 수 있는 새로운 형태의 복합 문(compound statement)을 제안합니다. 제안된 새 문 유형은 아직 사용될 키워드가 확정되지 않아 잠정적으로 ‘블록 문(block-statement)’이라고 불렸습니다.
이 PEP는 다음과 같은 다른 PEP들과 함께 고려되었습니다: PEP 288 (Generators Attributes and Exceptions), PEP 310 (Reliable Acquisition/Release Pairs), 그리고 PEP 325 (Resource-Release Support for Generators). 블록 문을 제너레이터(generator)로 ‘구동’하는 개념은 이 PEP의 핵심 아이디어였으며, 제너레이터 없이도 클래스를 사용해 유사한 기능을 구현할 수 있었지만, 제너레이터와의 결합을 통해 더 강력한 패턴을 제공하고자 했습니다. (참고로, PEP 342, Enhanced Iterators는 원래 이 PEP의 일부였으나 독립적인 제안으로 분리되었습니다.)
거부 공지 (Rejection Notice)
이 PEP는 PEP 343을 지지하여 거부되었습니다. 해당 결정의 배경은 PEP 343의 동기 부여 섹션에서 확인할 수 있습니다.
동기 및 요약 (Motivation and Summary)
(Shane Hathaway에게 감사를 표합니다.)
유능한 개발자는 자주 사용되는 코드를 재사용 가능한 함수로 만듭니다. 하지만 때로는 실제 코드의 순서보다는 함수의 구조에서 반복적인 패턴이 나타납니다. 예를 들어, 많은 함수가 록(lock)을 획득하고, 특정 작업을 수행한 다음, 무조건 록을 해제하는 패턴을 보입니다. 이러한 록 관련 코드를 매번 반복하는 것은 오류를 유발하기 쉽고 리팩토링(refactoring)을 어렵게 만듭니다.
블록 문은 이러한 구조적 패턴을 캡슐화하는 메커니즘을 제공합니다. 블록 문 내부의 코드는 ‘블록 이터레이터(block iterator)’라는 객체의 제어를 받습니다. 간단한 블록 이터레이터는 블록 내부 코드 실행 전후에 특정 코드를 실행할 수 있으며, 제어되는 코드를 여러 번 실행하거나 (또는 전혀 실행하지 않거나), 예외를 포착하거나, 블록 문 본문으로부터 데이터를 받을 수 있습니다.
블록 이터레이터를 구현하는 효과적인 방법은 제너레이터(PEP 255)를 활용하는 것입니다. 제너레이터는 일반 Python 함수와 유사하지만, 값을 즉시 반환하는 대신 yield
문에서 실행을 일시 중지합니다. 제너레이터가 블록 이터레이터로 사용될 때, yield
문은 Python 인터프리터에게 블록 이터레이터를 일시 중지하고, 블록 문 본문을 실행한 다음, 본문 실행이 완료되면 블록 이터레이터를 재개하도록 지시합니다.
제너레이터 기반 블록 문이 실행될 때 Python 인터프리터의 동작은 다음과 같습니다.
- 인터프리터는 제너레이터를 인스턴스화하고 실행을 시작합니다.
- 제너레이터는 록 획득, 파일 열기, 데이터베이스 트랜잭션 시작 등 캡슐화하는 패턴에 필요한 초기 설정 작업을 수행합니다.
- 이후 제너레이터는
yield
문을 통해 블록 문 본문으로 실행 제어를 넘겨줍니다. - 블록 문 본문이 완료되거나, 처리되지 않은 예외를 발생시키거나,
continue
문을 통해 제너레이터로 데이터를 보내면 제너레이터가 재개됩니다. - 이 시점에서 제너레이터는 정리 작업을 수행하고 종료하거나, 다시
yield
하여 블록 문 본문을 다시 실행시킬 수 있습니다. - 제너레이터의 실행이 완전히 끝나면 인터프리터는 블록 문을 빠져나갑니다.
사용 사례 (Use Cases)
이 PEP의 끝 부분에 있는 ‘예제(Examples)’ 섹션을 참조하십시오.
사양: __exit__()
메서드 (Specification: the __exit__()
Method)
이터레이터(iterator)를 위한 새로운 선택적 메서드 __exit__()
가 제안되었습니다. 이 메서드는 raise
문에 사용되는 세 가지 인수인 type
, value
, traceback
에 해당하는 최대 세 개의 인수를 받습니다. 만약 이 세 인수가 모두 None
이라면, sys.exc_info()
를 통해 적절한 기본값을 얻을 수 있습니다.
사양: 익명 블록 문 (Specification: the Anonymous Block Statement)
제안된 새로운 문법은 다음과 같습니다:
block EXPR1 as VAR1:
BLOCK1
여기서 'block'
과 'as'
는 새로운 키워드입니다. EXPR1
은 임의의 표현식 (단, 표현식 목록은 아님)이며, VAR1
은 임의의 할당 대상 (쉼표로 구분된 목록일 수 있음)입니다.
"as VAR1"
부분은 선택 사항입니다. 이 부분이 생략되면 아래의 변환에서 VAR1
에 대한 할당은 제외되지만, 할당될 표현식은 여전히 평가됩니다.
'block'
키워드의 선택은 많은 논의를 거쳤습니다. 키워드를 사용하지 않는 방식도 제안되었고, 심지어 Guido van Rossum 본인도 이 방식을 선호했습니다. PEP 310에서는 비슷한 의미로 'with'
를 사용했지만, with
문은 Pascal이나 VB와 같은 다른 언어의 with
문과 유사한 용도로 예약하고 싶다는 의견이 있었습니다. 'block'
키워드 자체도 변수, 인수, 메서드 이름으로 자주 사용되어 좋은 선택이 아니라는 지적이 있었습니다.
'as'
키워드는 논쟁의 여지가 없었고, 최종적으로 정식 키워드로 승격될 예정이었습니다.
블록 문이 여러 번 반복되는 루프를 나타낼지 여부는 이터레이터가 결정합니다. 대부분의 일반적인 사용 사례에서는 BLOCK1
이 정확히 한 번 실행됩니다. 그러나 파서(parser) 입장에서는 항상 루프로 처리되며, break
와 continue
는 블록의 이터레이터로 제어를 전달합니다.
이 블록 문의 변환은 for
루프와 미묘하게 다릅니다. iter()
함수가 호출되지 않으므로 EXPR1
은 이미 이터레이터(iterator)여야 합니다 (단순한 이터러블(iterable)이 아님). 또한, break
, return
또는 예외 발생 여부와 관계없이 블록 문이 종료될 때 이터레이터는 알림을 받도록 보장됩니다.
변환된 내부 동작은 다음과 같습니다:
itr = EXPR1 # 이터레이터
ret = False # return 문이 활성 상태이면 True
val = None # ret == True인 경우 반환 값
exc = None # 예외가 활성 상태이면 sys.exc_info() 튜플
while True:
try:
if exc:
ext = getattr(itr, "__exit__", None)
if ext is not None:
VAR1 = ext(*exc) # *exc를 다시 발생시킬 수 있음
else:
raise exc[0], exc[1], exc[2]
else:
VAR1 = itr.next() # 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 참조). 제너레이터 종료(finalization) 시맨틱은 (모든 종료 시맨틱의 한계 내에서) 블록이 결국 재개될 것임을 보장합니다.
for
루프와 달리 블록 문에는 else
절이 없습니다. 이는 블록 문의 “루프성”을 강조하여 혼란을 줄 수 있다고 판단했으며, 오히려 for
루프와의 차이점을 강조하고자 했습니다. 또한, else
절에 대한 여러 가능한 시맨틱이 존재하고, 그 사용 사례가 매우 미약하다는 점도 고려되었습니다.
사양: 제너레이터 종료 처리 (Specification: Generator Exit Handling)
제너레이터는 새로운 __exit__()
메서드 API를 구현할 것입니다.
제너레이터는 try-finally
문 내부에 yield
문을 가질 수 있도록 허용됩니다.
yield
문의 표현식 인수는 선택 사항이 됩니다 (기본값은 None
).
__exit__()
가 호출되면 제너레이터는 재개되지만, yield
문 지점에서 __exit__
인수로 전달된 예외가 발생합니다. 제너레이터는 이 예외를 다시 발생시키거나, 다른 예외를 발생시키거나, 다른 값을 yield
할 수 있습니다. 단, __exit__()
로 전달된 예외가 StopIteration
인 경우에는 StopIteration
을 다시 발생시켜야 합니다. (그렇지 않으면 break
가 continue
로 바뀌는 예상치 못한 동작을 초래할 수 있습니다.) 제너레이터를 재개하는 초기 호출이 next()
대신 __exit__()
호출인 경우, 제너레이터의 실행은 중단되고 제어를 제너레이터 본문에 전달하지 않고 예외가 다시 발생됩니다.
아직 종료되지 않은 제너레이터가 가비지 컬렉션(garbage-collected)될 때 (참조 카운팅 또는 주기적 가비지 컬렉터에 의해), StopIteration
을 첫 번째 인수로 사용하여 __exit__()
메서드가 한 번 호출됩니다. __exit__()
가 StopIteration
으로 호출될 때 제너레이터가 StopIteration
을 발생시켜야 한다는 요구 사항과 함께, 이는 제너레이터가 마지막으로 일시 중지되었을 때 활성화된 모든 finally
절이 결국 활성화되도록 보장합니다. 물론 특정 상황에서는 제너레이터가 가비지 컬렉션되지 않을 수도 있는데, 이는 다른 객체의 종료자(__del__()
메서드)에 대한 보장과 다르지 않습니다.
고려되었으나 거부된 대안 (Alternatives Considered and Rejected)
'block'
키워드에 대해 여러 대안이 제안되었지만, 더 나은 선택은 없었습니다. 하지만 'block'
자체도 변수, 인자, 메서드 이름으로 자주 사용되어 좋은 키워드는 아닙니다. 'with'
가 더 나은 선택일 수도 있습니다.
키워드를 사용하지 않고 다음과 같은 형식을 제안하기도 했습니다:
EXPR1 as VAR1:
BLOCK1
이 방식은 특정 함수 이름(EXPR1
에 사용된)과 함께 읽기 좋고 “사용자 정의 문”처럼 느껴져 처음에는 매력적이었습니다. 그러나 키워드가 없으면 구문이 “밋밋하고”, 설명서에서 찾아보기 어려우며 ('as'
가 선택 사항임을 고려), 블록 문에서 break
와 continue
의 의미를 더욱 혼란스럽게 만들었습니다.
Phillip Eby는 for
루프와 구별하기 위해 블록 문이 완전히 다른 API를 사용하도록 제안했습니다. 하지만 이는 이점이 적으면서 복잡성만 증가시키고, 블록 문이 break
와 continue
를 지원하는 한 개념적으로 루프라는 사실을 부정하기 어렵다는 반박이 있었습니다.
"block EXPR1 as VAR1"
대신 "block VAR1 = EXPR1"
과 같은 제안도 있었지만, VAR1
에 EXPR1
의 값이 직접 할당되지 않기 때문에 오해의 소지가 있어 기각되었습니다. 또한, iter(EXPR1)
를 적용하도록 변환을 변경하는 아이디어도 있었지만, 이는 블록 문을 for
루프와 더 유사하게 만들고 둘 사이의 차이점을 강조하려는 의도와 상충되어 기각되었습니다.
썽크(Thunks)와의 비교 (Comparison to Thunks)
블록 문에 대한 대안적인 시맨틱으로, 블록을 썽크(thunk, 포함하는 스코프에 녹아드는 익명 함수)로 바꾸는 아이디어가 있었습니다.
썽크의 주요 장점은 나중에 실행하기 위해 저장할 수 있다는 것입니다. 예를 들어 버튼 위젯의 콜백(callback)처럼 클로저(closure)가 될 수 있습니다. yield
기반 블록은 Ruby의 경우를 제외하고 이런 용도로 사용하기 어렵습니다. 하지만 Guido van Rossum은 블록이 정상적인 제어 흐름으로 실행될지 나중에 실행될지 모르는 것에 약간의 불편함을 느꼈고, 명시적인 중첩 함수를 정의하는 것이 더 명확하다고 생각했습니다.
썽크의 또 다른 문제는 썽크를 익명 함수로 간주하는 순간, 썽크 내의 return
문이 포함하는 함수가 아닌 썽크 자체에서 반환해야 한다는 제약이 생긴다는 것입니다. 이 경우 리소스 정리 템플릿 패턴과 같은 중요한 사용 사례가 제한될 수 있습니다. 예를 들어, 록을 획득하고 해제하는 findSomething
함수를 썽크를 사용해 재작성할 경우 return
문 처리 방식이 복잡해질 수 있습니다.
또한 썽크 내의 yield
는 썽크를 제너레이터로 바꾼다는 해석이 유일하게 합리적이었습니다.
Greg Ewing은 썽크가 “예외나 break
/continue
/return
문과 복잡하게 얽히지 않고 필요한 것만 수행하므로 훨씬 간단할 것”이라고 주장했습니다. 그러나 썽크와 포함하는 함수 간에 지역 변수를 공유하려면, 썽크에서 사용되거나 설정되는 모든 지역 변수가 ‘셀(cell)’이 되어야 하는데, 이는 일반 지역 변수보다 접근 속도를 늦출 수 있습니다.
Greg Ewing은 제너레이터가 한 번에 여러 개를 실행할 수 있어 더 강력하다고 보았으며, 이 기능이 블록 문에서도 유용할 수 있다고 언급했습니다. 실제로 많은 개발자가 제너레이터를 사용해 비동기 경량 스레드를 구현하는 방법을 보여주었습니다.
마지막으로 Greg Ewing은 썽크 구현이 적절한 구문이 마련된다면 여러 블록 인수를 쉽게 처리할 수 있는 잠재력이 있다고 언급했지만, 여러 블록에 대한 사용 사례는 명확하지 않았습니다.
(이후 썽크 구현에 대한 여러 제안이 있었지만, 그 결과로 나타나는 시맨틱이 복잡하여 썽크를 사용하는 본래 목적을 훼손한다고 판단되었습니다.)
예제 (Examples)
(일부 예제에 "yield None"
이 포함되어 있습니다. PEP 342가 수락되었다면 단순히 "yield"
로 변경될 수 있었습니다.)
- 록(Lock) 관리 템플릿: 블록 시작 시 획득된 록이 블록 종료 시 (예외 발생 시에도) 항상 해제되도록 보장합니다.
def locking(lock): lock.acquire() try: yield None finally: lock.release() # 사용 예: block locking(myLock): # myLock이 잡힌 상태에서 코드가 실행됩니다. # 블록 종료 시 록 해제가 보장됩니다.
- 파일 열기 템플릿: 파일을 열고, 블록 종료 시 파일이 항상 닫히도록 보장합니다.
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()
- 데이터베이스 트랜잭션 템플릿: 블록 내 작업 성공 시 커밋, 실패 시 롤백을 수행합니다.
def transactional(db): try: yield None except: db.rollback() raise else: db.commit()
- 최대 N회 재시도 템플릿: 특정 예외 발생 시 작업을 최대 N번 재시도합니다.
def auto_retry(n=3, exc=Exception): for i in range(n): try: yield None return except exc, err: # 여기서 예외를 로깅할 수 있습니다. continue raise # 이전에 포착한 예외를 다시 발생시킵니다. # 사용 예: block auto_retry(3, IOError): f = urllib.urlopen("https://www.example.com/") print f.read()
- 블록 중첩 및 템플릿 결합: 여러 블록 템플릿을 중첩하여 사용할 수 있습니다.
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()
- 정규 이터레이터를 이용한 록 관리 구현: 제너레이터 대신 클래스 기반의 정규 이터레이터로 록 관리 시맨틱을 구현한 예시입니다. 제너레이터가 얼마나 더 간단한지 보여줍니다.
class locking: def __init__(self, lock): self.lock = lock self.state = 0 def __next__(self, arg=None): 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
- 표준 출력(stdout) 임시 리디렉션: 특정 블록 내에서
sys.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"
- 오류 조건을 반환하는
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")
감사의 글 (Acknowledgements)
Alex Martelli, Barry Warsaw, Bob Ippolito 등 많은 이들이 이 PEP에 귀중한 기여를 했습니다.
참고 자료 (References)
https://mail.python.org/pipermail/python-dev/2005-April/052821.html https://web.archive.org/web/20060719195933/http://msdn.microsoft.com/vcsharp/programming/language/ask/withstatement/ https://web.archive.org/web/20050204062901/http://effbot.org/zone/asyncore-generators.htm
저작권 (Copyright)
이 문서는 공개 도메인에 게시되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments