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

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 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에서 설명하는 해법의 기본 개요도 제안했습니다.

참고 문헌