PEP 340 – 익명 블록 문
- Author:
- Guido van Rossum
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 27-Apr-2005
- Post-History:
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
소개
이 PEP는 자원 관리 목적으로 사용할 수 있는 새로운 종류의 복합문을 제안합니다. 사용할 키워드가 아직 정해지지 않았으므로, 이 새로운 문 유형을 잠정적으로 블록 문(block-statement)이라고 부릅니다.
이 PEP는 PEP 288 (제너레이터 속성과 예외; 두 번째 부분만 해당), PEP 310 (신뢰할 수 있는 획득/해제 쌍), PEP 325 (제너레이터를 위한 자원 해제 지원) 등 다른 여러 PEP와 경쟁합니다.
제너레이터를 사용해 블록 문을 “구동”하는 것은 사실 분리 가능한 제안이라는 점을 분명히 해 두어야겠습니다. 이 PEP에 있는 블록 문의 정의만 있으면 모든 예제를 클래스로 구현할 수 있습니다(쉽게 템플릿으로 바꿀 수 있는 예제 6과 비슷한 방식입니다). 하지만 핵심 아이디어는 제너레이터를 사용해 블록 문을 구동하는 것이고 나머지는 부연에 불과하므로, 이 두 부분을 함께 두고자 합니다.
(PEP 342, 향상된 이터레이터는 원래 이 PEP의 일부였지만, 두 제안은 사실 서로 독립적이므로 Steven Bethard의 도움을 받아 별도의 PEP로 옮겼습니다.)
거부 공지
PEP 343을 채택하기로 하고 이 PEP를 거부합니다. 이 거부의 이유는 해당 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 문은 블록의 이터레이터에 알리지 않고 블록을 포함하는 함수를 일시 중단합니다. 지역 제어 흐름이 실제로 블록을 벗어나지 않으므로, 블록의 이터레이터는 이 yield를 전혀 알지 못합니다. 다시 말해, yield 문은 break 문이나 return 문과 같지 않습니다. yield로 인해 재개된 루프가 next()를 호출하면, 블록은 yield 바로 다음부터 재개됩니다. (아래 예제 7을 참조하십시오.) 아래에서 설명하는 제너레이터 종료 처리 의미론은 (모든 종료 처리 의미론이 갖는 한계 안에서) 블록이 결국 재개될 것을 보장합니다.
for 루프와 달리 블록 문에는 else 절이 없습니다. 저는 else 절이 혼란을 줄 뿐 아니라 블록 문의 “루프다움”을 부각할 것이라고 생각하는데, 저는 오히려 블록 문과 for 루프의 차이점을 강조하고 싶습니다. 또한 else 절에는 가능한 의미론이 여러 가지인 데 비해, 사용 사례는 매우 빈약합니다.
명세: 제너레이터 종료 처리
제너레이터는 새로운 __exit__() 메서드 API를 구현할 것입니다.
제너레이터는 try-finally 문 안에 yield 문을 가질 수 있게 됩니다.
yield 문의 표현식 인자는 선택 사항이 됩니다(기본값은 None입니다).
__exit__()가 호출되면 제너레이터가 재개되지만, yield 문 지점에서 __exit__ 인자(들)가 나타내는 예외가 발생합니다. 제너레이터는 이 예외를 다시 발생시키거나, 다른 예외를 발생시키거나, 다른 값을 yield할 수 있습니다. 다만 __exit__()에 전달된 예외가 StopIteration이었다면 StopIteration을 발생시켜야 합니다(그렇지 않으면 break가 continue로 바뀌는 효과가 생기는데, 이는 적어도 예상 밖의 일입니다). 제너레이터를 재개하는 최초의 호출이 next() 호출이 아니라 __exit__() 호출이면, 제너레이터의 실행은 중단되고 제너레이터 본문으로 제어를 넘기지 않은 채 예외가 다시 발생합니다.
아직 종료되지 않은 제너레이터가 (참조 카운팅에 의해서든 순환 가비지 수집기에 의해서든) 가비지 수집되면, 그 제너레이터의 __exit__() 메서드가 StopIteration을 첫 번째 인자로 하여 한 번 호출됩니다. __exit__()가 StopIteration과 함께 호출되면 제너레이터가 StopIteration을 발생시켜야 한다는 요구 사항과 더불어, 이는 제너레이터가 마지막으로 일시 중단되었을 때 활성 상태였던 모든 finally 절이 결국 실행됨을 보장합니다. 물론 특정 상황에서는 제너레이터가 끝내 가비지 수집되지 않을 수도 있습니다. 이는 다른 객체의 파이널라이저(__del__() 메서드)에 대해 제공되는 보장과 다르지 않습니다.
검토했으나 기각된 대안
- ‘block’에 대해 많은 대안이 제안되었습니다. 아직 ‘block’보다 더 마음에 드는 다른 키워드 제안은 보지 못했습니다. 안타깝게도 ‘block’ 역시 좋은 선택이 아닙니다. 변수, 인자, 메서드의 이름으로 꽤 널리 쓰이기 때문입니다. 어쩌면 결국 ‘with’가 최선의 선택일지도 모릅니다.
- 이상적인 키워드를 고르려 애쓰는 대신, block 문이 단순히 다음과 같은 형태를 가질 수도 있습니다.:
EXPR1 as VAR1: BLOCK1
이 방식은 처음에는 매력적입니다.
EXPR1에 사용되는 함수 이름을 (아래 예제 절에 있는 것들처럼) 잘 고르면 잘 읽히고, “사용자 정의 문”처럼 느껴지기 때문입니다. 그런데도 이 방식은 저를(그리고 다른 많은 사람을) 불편하게 합니다. 키워드가 없으면 문법이 매우 “밋밋”하고, 매뉴얼에서 찾아보기 어려우며(‘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로 사용하는 것과 같은 여러 오해를 잡아낼 수 있습니다.
썽크와의 비교
block 문에 대해 제안된 대안적 의미론은 블록을 썽크(자신을 둘러싼 스코프에 녹아드는 익명 함수)로 만듭니다.
제가 보기에 썽크의 주된 장점은 버튼 위젯의 콜백처럼 썽크를 나중에 쓰려고 저장해 둘 수 있다는 점입니다(이때 썽크는 클로저가 됩니다). yield 기반 블록은 그런 용도로 사용할 수 없습니다(yield 문법을 쓰면서 썽크 기반으로 구현한 Ruby는 예외입니다). 하지만 저는 이것을 거의 장점으로 본다고 말하지 않을 수 없습니다. 블록을 보면서 그것이 일반적인 제어 흐름 안에서 실행될지 나중에 실행될지 알 수 없다면 조금 불편할 것 같기 때문입니다. 그런 목적으로 명시적인 중첩 함수를 정의하는 것은 제게 이런 문제가 없습니다. ‘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가 갖는 의미론적 난제에도 주목하십시오. 유일하게 합리적인 해석은 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”로 바꿀 수 있습니다.)
- 블록 시작 시점에 획득한 잠금이 블록을 벗어날 때 해제되도록 보장하는 템플릿:
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).
- 블록을 벗어날 때 파일이 닫히도록 보장하면서 파일을 여는 템플릿:
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: # 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()
- 블록을 중첩하고 템플릿을 조합할 수 있습니다:
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): # 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
(이 예제는 다른 예제들을 구현하도록 쉽게 수정할 수 있습니다. 같은 목적이라면 제너레이터가 얼마나 더 간단한지 보여 줍니다.)
- 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")
감사의 글
순서는 특별한 의미가 없습니다: 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
Copyright
This document has been placed in the public domain.