PEP 377 – __enter__() 메서드가 문 본문을 건너뛸 수 있도록 허용
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 08-Mar-2009
- Python-Version:
- 2.7, 3.1
- Post-History:
- 08-Mar-2009
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
개요
이 PEP는 __enter__() 메서드가 연관된 with 문의 본문을 건너뛸 수 있도록 하는 하위 호환 메커니즘을 제안합니다. 이 능력이 없기 때문에 현재 contextlib.contextmanager 데코레이터는 임의의 코드를 적절한 위치에 yield가 있는 제너레이터 함수로 옮겨 컨텍스트 관리자로 바꿀 수 있다는 명세를 충족하지 못합니다. 이 증상 중 하나는, 대응하는 중첩 with 문을 직접 작성했다면 발생하지 않았을 상황에서 contextlib.nested가 현재 RuntimeError를 발생시킨다는 것입니다 [1].
제안된 변경 사항은 새로운 흐름 제어 예외 SkipStatement를 도입하고, __enter__()가 이 예외를 발생시키면 with 문 본문의 실행을 건너뛰는 것입니다.
PEP 거부
이 PEP는 표현력과 정확성의 비례적인 증가 없이 지나치게 큰 복잡성 증가를 초래한다는 이유로 Guido에 의해 거부되었습니다 [4]. 이 PEP가 제안하는 더 복잡한 의미론을 필요로 하는 설득력 있는 사용 사례가 없는 상황에서는 기존 동작이 받아들일 만한 것으로 간주됩니다.
제안된 변경 사항
with 문의 의미론은 __enter__() 호출을 감싸는 새로운 try/except/else 블록을 포함하도록 변경될 것입니다. __enter__() 메서드가 SkipStatement를 발생시키면, with 문의 주요 부분(이제 else 절에 위치함)은 실행되지 않습니다. 이 경우 as 절에 있는 이름들이 바인딩되지 않은 채로 남지 않도록, 새로운 StatementSkipped 싱글턴(기존 NotImplemented 싱글턴과 유사함)이 as 절에 나타나는 모든 이름에 할당됩니다.
with 문의 구성 요소는 PEP 343에 설명된 대로 유지됩니다.:
with EXPR as VAR:
BLOCK
수정 후, with 문의 의미론은 다음과 같습니다.:
mgr = (EXPR)
exit = mgr.__exit__ # Not calling it yet
try:
value = mgr.__enter__()
except SkipStatement:
VAR = StatementSkipped
# Only if "as VAR" is present and
# VAR is a single name
# If VAR is a tuple of names, then StatementSkipped
# will be assigned to each name in the tuple
else:
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(*sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(None, None, None)
with 문 의미론에 대한 위의 변경이 적용된 상태에서, contextlib.contextmanager()는 기저의 제너레이터가 yield하지 않을 때 RuntimeError 대신 SkipStatement를 발생시키도록 수정됩니다.
변경 근거
현재, 겉보기에는 무해해 보이는 일부 컨텍스트 관리자가 실행 시 RuntimeError를 발생시킬 수 있습니다. 이는 컨텍스트 관리자의 __enter__() 메서드가, 해당 컨텍스트 관리자에 대응하는 코드를 풀어썼을 때 이제 with 문의 본문이 된 코드를 건너뛰게 되는 상황을 만났을 때 발생합니다. __enter__() 메서드에는 이를 인터프리터에 알릴 수단이 없기 때문에, 대신 with 문의 본문을 건너뛸 뿐만 아니라 가장 가까운 예외 처리기까지 모든 코드를 건너뛰는 예외를 발생시킬 수밖에 없습니다. 이는 with 문의 설계 목표 중 하나, 즉 임의의 공통 예외 처리 코드를 제너레이터 함수 안에 넣고 코드의 가변 부분을 yield 문으로 대체함으로써 단일 컨텍스트 관리자로 추출해낼 수 있어야 한다는 목표에 어긋납니다.
구체적으로, 다음 예제들은 cmB().__enter__()가 예외를 발생시키고 이를 cmA().__exit__()가 처리하여 억제하는 경우 서로 다르게 동작합니다:
with cmA():
with cmB():
do_stuff()
# This will resume here without executing "do_stuff()"
@contextlib.contextmanager
def combined():
with cmA():
with cmB():
yield
with combined():
do_stuff()
# This will raise a RuntimeError complaining that the context
# manager's underlying generator didn't yield
with contextlib.nested(cmA(), cmB()):
do_stuff()
# This will raise the same RuntimeError as the contextmanager()
# example (unsurprising, given that the nested() implementation
# uses contextmanager())
# The following class based version shows that the issue isn't
# specific to contextlib.contextmanager() (it also shows how
# much simpler it is to write context managers as generators
# instead of as classes!)
class CM(object):
def __init__(self):
self.cmA = None
self.cmB = None
def __enter__(self):
if self.cmA is not None:
raise RuntimeError("Can't re-use this CM")
self.cmA = cmA()
self.cmA.__enter__()
try:
self.cmB = cmB()
self.cmB.__enter__()
except:
self.cmA.__exit__(*sys.exc_info())
# Can't suppress in __enter__(), so must raise
raise
def __exit__(self, *args):
suppress = False
try:
if self.cmB is not None:
suppress = self.cmB.__exit__(*args)
except:
suppress = self.cmA.__exit__(*sys.exc_info()):
if not suppress:
# Exception has changed, so reraise explicitly
raise
else:
if suppress:
# cmB already suppressed the exception,
# so don't pass it to cmA
suppress = self.cmA.__exit__(None, None, None):
else:
suppress = self.cmA.__exit__(*args):
return suppress
제안된 의미 변경이 적용되면, 위의 contextlib 기반 예제들은 “그대로 작동”하게 되지만, 클래스 기반 버전은 새로운 의미론을 활용하기 위해 약간의 조정이 필요합니다:
class CM(object):
def __init__(self):
self.cmA = None
self.cmB = None
def __enter__(self):
if self.cmA is not None:
raise RuntimeError("Can't re-use this CM")
self.cmA = cmA()
self.cmA.__enter__()
try:
self.cmB = cmB()
self.cmB.__enter__()
except:
if self.cmA.__exit__(*sys.exc_info()):
# Suppress the exception, but don't run
# the body of the with statement either
raise SkipStatement
raise
def __exit__(self, *args):
suppress = False
try:
if self.cmB is not None:
suppress = self.cmB.__exit__(*args)
except:
suppress = self.cmA.__exit__(*sys.exc_info()):
if not suppress:
# Exception has changed, so reraise explicitly
raise
else:
if suppress:
# cmB already suppressed the exception,
# so don't pass it to cmA
suppress = self.cmA.__exit__(None, None, None):
else:
suppress = self.cmA.__exit__(*args):
return suppress
현재 contextlib.nested를 사용할 필요 없이 하나의 with 문에 여러 컨텍스트 관리자를 포함할 수 있도록, with 문에 import 스타일 문법을 추가하자는 잠정적인 제안 [3]이 있습니다. 이 경우 컴파일러는 AST 수준에서 단순히 여러 개의 with 문을 내보내는 선택지를 가지게 되어, 실제로 중첩된 with 문의 의미론을 정확하게 재현할 수 있습니다. 그러나 이러한 변경은 현재 이 PEP가 해결하고자 하는 문제를 완화하기보다는 오히려 부각시키게 됩니다: 이런 with 문들은 위 예제의 combined() 컨텍스트 관리자에서 나타나는 것과 정확히 같은 의미 차이를 보이기 때문에, contextlib.contextmanager를 사용하여 이를 안정적으로 추출해내는 것이 불가능해집니다.
성능에 미치는 영향
새로운 의미론을 구현하려면 __enter__와 __exit__ 메서드에 대한 참조를 스택이 아닌 임시 변수에 저장해야 합니다. 이로 인해 Python 2.6/3.1에 비해 with 문의 속도가 약간 저하됩니다. 그러나 커스텀 SETUP_WITH 옵코드를 구현하면 두 접근 방식 간의 차이가 사라질 뿐만 아니라 (eval 루프를 열두 번 넘게 불필요하게 도는 것을 제거함으로써 속도도 극적으로 향상됩니다).
참조 구현
Issue 5251 [1]에 첨부된 패치입니다. 이 패치는 기존 옵코드만 사용합니다(즉, SETUP_WITH는 없습니다).
감사의 글
James William Pye는 이 문제를 제기했을 뿐만 아니라 이 PEP에서 설명하는 해법의 기본 개요도 제안했습니다.
참고 문헌
Copyright
This document has been placed in the public domain.