PEP 343 – “with” 문
- Author:
- Guido van Rossum, Alyssa Coghlan
- Status:
- Final
- Type:
- Standards Track
- Created:
- 13-May-2005
- Python-Version:
- 2.5
- Post-History:
- 02-Jun-2005, 16-Oct-2005, 29-Oct-2005, 23-Apr-2006, 01-May-2006, 30-Jul-2006
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 try/finally 문의 표준적인 사용 방식을 분리해 낼 수 있도록 Python 언어에 새로운 문 “with”를 추가합니다.
이 PEP에서 컨텍스트 관리자는 with 문의 본문에 진입할 때와 본문에서 빠져나올 때 호출되는 __enter__()와 __exit__() 메서드를 제공합니다.
저자의 말
이 PEP는 원래 Guido가 1인칭으로 작성했으며, 이후 python-dev에서 이루어진 논의를 반영하도록 Alyssa (Nick) Coghlan이 갱신했습니다. 1인칭으로 언급된 부분은 모두 Guido의 원문에서 온 것입니다.
Python의 알파 릴리스 주기 동안 이 PEP와 관련 문서 및 구현에서 용어 문제가 드러났습니다 [13]. 이 PEP는 Python 2.5의 첫 베타 릴리스 무렵에 안정화되었습니다.
네, 몇몇 곳에서 동사 시제가 뒤죽박죽입니다. 우리는 이 PEP를 1년 넘게 작업해 왔기 때문에, 원래 미래의 일이었던 것들이 이제는 과거의 일이 되었습니다 :)
소개
PEP 340과 그 대안들에 대해 많은 논의를 거친 끝에, 저는 PEP 340을 철회하고 PEP 310을 약간 변형한 안을 제안하기로 했습니다. 추가 논의를 거쳐, 일시 중단된 제너레이터 안에서 throw() 메서드를 사용해 예외를 발생시키는 메커니즘과 새로운 GeneratorExit 예외를 던지는 close() 메서드를 다시 추가했습니다. 이 추가 사항들은 python-dev에서 처음 제안되었으며 [2], 모두의 찬성을 받았습니다. 또한 키워드를 ‘with’로 변경합니다.
이 PEP가 수락된 후, 다음 PEP들은 내용이 겹친다는 이유로 거부되었습니다:
- PEP 310, 신뢰할 수 있는 획득/해제 쌍. 이것은 최초의 with 문 제안입니다.
- PEP 319, Python 동기화/비동기화 블록. 적절한 with 문 컨트롤러를 제공하면 그 사용 사례를 현재 PEP로 처리할 수 있습니다. ‘synchronize’에는 예제 1의 “locking” 템플릿을 사용할 수 있고, ‘asynchronize’에는 이와 비슷한 “unlocking” 템플릿을 사용할 수 있습니다. 저는 코드 블록에 “익명” 잠금이 연결되는 것이 그다지 중요하다고 생각하지 않습니다. 오히려 사용되는 뮤텍스를 항상 명시하는 편이 더 나을 수도 있습니다.
PEP 340과 PEP 346도 이 PEP와 겹쳤지만, 이 PEP가 제출되었을 때 자발적으로 철회되었습니다.
이 PEP의 초기 형태에 대한 일부 논의는 Python 위키 [3]에서 이루어졌습니다.
동기와 요약
PEP 340, 익명 블록 문은 제너레이터를 블록 템플릿으로 사용하는 것, 제너레이터에 예외 처리와 종료 처리를 추가하는 것 등 여러 강력한 아이디어를 결합했습니다. 칭찬도 받았지만, 그것이 내부적으로는 (잠재적인) 반복 구조라는 사실을 좋아하지 않는 사람들로부터 많은 반대도 받았습니다. 이는 블록 문을 반복하지 않는 자원 관리 도구로 사용하더라도, 블록 문 안의 break와 continue가 그 블록 문을 중단하거나 계속한다는 것을 의미했습니다.
그러나 결정타는 흐름 제어 매크로에 대한 Raymond Chen의 비판 글 [1]을 읽었을 때 찾아왔습니다. Raymond는 흐름 제어를 매크로 안에 숨기면 코드를 이해할 수 없게 된다고 설득력 있게 주장하며, 저는 그의 인자(논거)가 C뿐만 아니라 Python에도 적용된다고 생각합니다. 저는 PEP 340 템플릿이 온갖 종류의 제어 흐름을 숨길 수 있다는 것을 깨달았습니다. 예를 들어, 그 PEP의 예제 4(auto_retry())는 예외를 잡아 블록을 최대 세 번까지 반복합니다.
그러나 제가 보기에 PEP 310의 with 문은 제어 흐름을 숨기지 않습니다. finally 스위트가 제어 흐름을 일시적으로 중단시키기는 하지만, 결국에는 finally 스위트가 전혀 없었던 것처럼 제어 흐름이 재개됩니다.
기억하십시오, PEP 310은 대략 다음과 같은 문법을 제안합니다(“VAR =” 부분은 선택 사항입니다).:
with VAR = EXPR:
BLOCK
이는 대략 다음과 같이 변환됩니다.:
VAR = EXPR
VAR.__enter__()
try:
BLOCK
finally:
VAR.__exit__()
이제 다음 예제를 살펴봅시다.:
with f = open("/etc/passwd"):
BLOCK1
BLOCK2
여기서는 첫 줄이 “if True”였을 때와 마찬가지로, BLOCK1이 예외 없이 완료되면 BLOCK2에 도달하고, BLOCK1이 예외를 일으키거나 비지역 goto(break, continue 또는 return)를 실행하면 BLOCK2에는 도달하지 않는다는 것을 알 수 있습니다. 끝부분에서 with 문이 추가하는 마법은 이에 영향을 주지 않습니다.
(__exit__() 메서드의 버그가 예외를 일으키면 어떻게 되느냐고 물을 수도 있습니다. 그러면 모든 것이 끝장입니다. 하지만 이는 다른 예외의 경우보다 나쁘지 않습니다. 예외는 본질적으로 어디서든 발생할 수 있으며, 그 사실을 그냥 받아들이고 살아야 합니다. 버그 없는 코드를 작성하더라도, KeyboardInterrupt 예외 때문에 임의의 두 가상 머신 옵코드 사이에서 코드가 종료될 수 있습니다.)
이 인자(논거) 때문에 하마터면 PEP 310을 지지할 뻔했지만, PEP 340에 대한 열광에서 남은 아이디어 하나를 아직 버릴 준비가 되어 있지 않았습니다. 바로 잠금을 획득하고 해제하거나 파일을 열고 닫는 것과 같은 추상화를 위한 “템플릿”으로 제너레이터를 사용하는 것은 강력한 아이디어라는 점이며, 이는 그 PEP의 예제들을 살펴보면 알 수 있습니다.
Phillip Eby가 PEP 340에 대해 내놓은 대안 제안에서 영감을 받아, 저는 적절한 제너레이터를 필요한 __enter__() 및 __exit__() 메서드를 갖춘 객체로 바꾸는 데코레이터를 만들어 보았습니다. 여기서 난관에 부딪혔습니다. 잠금 예제에서는 그리 어렵지 않았지만, 파일 열기 예제에서는 이를 구현하는 것이 불가능했습니다. 템플릿을 다음과 같이 정의하려는 생각이었습니다.:
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
그리고 다음과 같이 사용했습니다.:
with f = opening(filename):
...read data from f...
문제는 PEP 310에서 EXPR을 호출한 결과가 VAR에 직접 할당되고, BLOCK1에서 빠져나올 때 VAR의 __exit__() 메서드가 호출된다는 점입니다. 하지만 여기서는 VAR이 분명히 열린 파일을 받아야 하며, 이는 곧 __exit__() 메서드가 파일의 메서드여야 한다는 뜻입니다.
이 문제는 프록시 클래스를 사용하여 해결할 수 있지만 번거로운 방법이었고, 이를 계기로 변환 방식을 조금 달리하면 원하는 데코레이터를 아주 쉽게 작성할 수 있다는 것을 깨달았습니다. 즉, VAR이 __enter__() 메서드를 호출한 결과를 받도록 하고, 나중에 __exit__() 메서드를 호출할 수 있도록 EXPR의 값을 저장해 두는 것입니다. 그러면 데코레이터는 래퍼 클래스의 인스턴스를 반환할 수 있습니다. 이 래퍼 클래스의 __enter__() 메서드는 제너레이터의 next() 메서드를 호출하여 next() 호출이 반환하는 값을 그대로 반환하고, 래퍼 인스턴스의 __exit__() 메서드는 next() 메서드를 다시 호출하되 StopIteration이 발생하기를 기대합니다. (자세한 내용은 아래의 선택적 제너레이터 데코레이터 절을 참고하십시오.)
그래서 이제 마지막 장애물은 PEP 310의 문법이:
with VAR = EXPR:
BLOCK1
이 구문은 VAR이 EXPR의 값을 받지 않으므로 오해를 불러일으킬 수 있습니다. PEP 340에서 빌려 와 다음과 같이 바꾸는 것은 쉬운 일이었습니다.:
with EXPR as VAR:
BLOCK1
추가 논의를 통해, 단지 로그를 남기기 위해서라도 사람들이 제너레이터 안에서 예외를 “볼” 수 있는 것을 무척 좋아한다는 사실이 드러났습니다. with 문을 루프로 사용할 수 있어서는 안 되므로 제너레이터가 다른 값을 yield하는 것은 허용되지 않습니다(다른 예외를 일으키는 것은 가까스로 허용됩니다). 이를 가능하게 하기 위해 제너레이터에 새로운 throw() 메서드를 제안합니다. 이 메서드는 일반적인 방식(type, value, traceback)으로 예외를 나타내는 인자를 한 개에서 세 개까지 받아서, 제너레이터가 일시 중단된 지점에서 그 예외를 일으킵니다.
이것이 갖춰지면, 특별한 예외인 GeneratorExit을 인자로 throw()를 호출하는 또 다른 제너레이터 메서드 close()를 제안하는 것은 작은 단계에 불과합니다. 이 메서드는 제너레이터에 종료하라고 알리며, 거기서 제너레이터가 가비지 수집될 때 close()가 자동으로 호출되도록 제안하는 것 역시 작은 단계입니다.
그러면 마침내 try-finally 문 안에서 yield 문을 허용할 수 있습니다. 이제 finally 절이 (결국에는) 실행된다는 것을 보장할 수 있기 때문입니다. 종료 처리(finalization)에 관한 일반적인 주의 사항이 적용됩니다. 프로세스는 어떤 객체도 종료 처리하지 않은 채 갑자기 종료될 수 있으며, 애플리케이션의 순환 참조나 메모리 누수 때문에 객체가 영원히 살아 있을 수도 있습니다(GC가 처리하는 Python 구현 내부의 순환 참조나 누수와는 다릅니다).
CPython에서는 그렇게 동작하겠지만, 제너레이터 객체가 더 이상 사용되지 않게 된 직후에 finally 절이 실행된다고 보장하지는 않는다는 점에 유의하십시오. 이는 파일 자동 닫기와 비슷합니다. CPython처럼 참조 카운팅을 사용하는 구현은 객체에 대한 마지막 참조가 사라지는 즉시 객체를 해제하지만, 다른 GC 알고리즘을 사용하는 구현은 같은 보장을 하지 않습니다. 이는 Jython, IronPython, 그리고 아마도 Parrot 위에서 실행되는 Python에도 해당합니다.
(제너레이터에 가해진 변경 사항의 자세한 내용은 이제 현재 PEP가 아니라 PEP 342에서 찾을 수 있습니다)
사용 사례
끝부분의 예제 절을 참고하십시오.
명세: ‘with’ 문
다음과 같은 문법을 가진 새로운 문을 제안합니다.:
with EXPR as VAR:
BLOCK
여기서 ‘with’와 ‘as’는 새로운 키워드이며, EXPR은 임의의 표현식(단, 표현식 목록은 아님)이고 VAR는 단일 대입 대상입니다. 이것은 쉼표로 구분된 변수 시퀀스일 수는 없지만, 괄호로 묶인 쉼표 구분 변수 시퀀스일 수는 있습니다. (이 제약 덕분에 앞으로 쉼표로 구분된 여러 자원을 가지되 각 자원이 선택적인 as 절을 따로 가질 수 있도록 문법을 확장할 수 있습니다.)
“as VAR” 부분은 선택 사항입니다.
위 문의 변환 결과는 다음과 같습니다.:
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
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(mgr, *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(mgr, None, None, None)
여기서 소문자 변수(mgr, exit, value, exc)는 내부 변수이며 사용자가 접근할 수 없습니다. 이 변수들은 특수 레지스터나 스택 위치로 구현될 가능성이 가장 높습니다.
위 변환의 세부 사항은 정확한 의미를 규정하기 위한 것입니다. 관련 메서드 중 어느 하나라도 예상대로 찾을 수 없으면, 인터프리터는 시도하는 순서(__exit__, __enter__)대로 AttributeError를 발생시킵니다. 마찬가지로 호출 중 어느 하나라도 예외를 발생시키면, 그 효과는 위 코드에서와 정확히 같습니다. 마지막으로 BLOCK에 break, continue 또는 return 문이 있으면, BLOCK이 정상적으로 완료된 것과 똑같이 __exit__() 메서드가 세 개의 None 인자로 호출됩니다. (즉, 이러한 “의사 예외”는 __exit__()에서 예외로 간주되지 않습니다.)
문법에서 “as VAR” 부분이 생략되면 변환 결과에서 “VAR =” 부분도 생략됩니다(그러나 mgr.__enter__()는 여전히 호출됩니다).
mgr.__exit__()의 호출 규약은 다음과 같습니다. BLOCK이 정상적으로 완료되었거나 비지역 goto(BLOCK 안의 break, continue 또는 return 문)를 통해 finally 스위트에 도달한 경우, mgr.__exit__()는 세 개의 None 인자로 호출됩니다. BLOCK에서 발생한 예외를 통해 finally 스위트에 도달한 경우, mgr.__exit__()는 예외 타입, 값, 트레이스백을 나타내는 세 개의 인자로 호출됩니다.
중요: mgr.__exit__()가 “참” 값을 반환하면 예외는 “삼켜집니다”. 즉, “참”을 반환하면 with 문 안에서 예외가 발생했더라도 with 문 다음 문에서 실행이 계속됩니다. 그러나 with 문을 비지역 goto(break, continue 또는 return)를 통해 빠져나간 경우에는, 반환값과 관계없이 mgr.__exit__()가 반환될 때 이 비지역 반환이 재개됩니다. 이렇게 세부 사항을 정한 동기는 mgr.__exit__()가 예외를 삼킬 수 있게 하되 너무 쉽게 삼키지는 않도록 하려는 것입니다(기본 반환값인 None은 거짓이므로 예외가 다시 발생합니다). 예외를 삼키는 주된 사용 사례는, 데코레이트된 제너레이터 안의 try/except 블록이 마치 제너레이터 본문이 with 문 위치에 인라인으로 펼쳐진 것과 정확히 똑같이 동작하도록 @contextmanager 데코레이터를 작성할 수 있게 하는 것입니다.
PEP 310의 인자 없는 __exit__()와 달리 __exit__()에 예외 세부 정보를 전달하는 동기는 아래 예제 3인 transactional() 사용 사례에서 비롯되었습니다. 해당 예제의 템플릿은 예외 발생 여부에 따라 트랜잭션을 커밋하거나 롤백해야 합니다. 예외 발생 여부를 나타내는 불리언 플래그만 두는 대신, 예를 들어 예외 로깅 기능에 도움이 되도록 완전한 예외 정보를 전달합니다. 예외 정보를 얻기 위해 sys.exc_info()에 의존하는 방식은 기각되었습니다. sys.exc_info()는 의미가 매우 복잡하며, 아주 오래전에 잡힌 예외의 정보를 반환하는 일도 충분히 있을 수 있습니다. BLOCK의 끝에 도달한 경우와 비지역 goto를 구분하기 위해 불리언을 하나 더 추가하자는 제안도 있었습니다. 이 제안은 너무 복잡하고 불필요하다는 이유로 기각되었습니다. 데이터베이스 트랜잭션 롤백 결정에서 비지역 goto는 예외적인 상황이 아닌 것으로 간주해야 합니다.
컨텍스트 관리자를 직접 조작하는 Python 코드에서 컨텍스트를 연쇄하기 쉽도록, __exit__() 메서드는 자신에게 전달된 오류를 다시 발생시켜서는 안 됩니다. 그런 경우 다시 발생시키는 일은 언제나 __exit__() 메서드 호출자의 책임입니다.
그렇게 하면 호출자는 __exit__() 호출이 (원래 오류를 전파하기 전에 성공적으로 정리한 것이 아니라) 실패했는지 알아내야 할 때 이를 알아낼 수 있습니다.
__exit__()가 오류 없이 반환되면, (원래 오류를 전파할지 억제할지와 관계없이) 이를 __exit__() 메서드 자체의 성공으로 해석할 수 있습니다.
그러나 __exit__()가 호출자에게 예외를 전파한다면, 이는 __exit__() 자체가 실패했다는 뜻입니다. 따라서 __exit__() 메서드는 실제로 실패한 경우가 아니라면 오류를 발생시키지 않아야 합니다. (그리고 원래 오류가 계속 진행되도록 두는 것은 실패가 아닙니다.)
전환 계획
Python 2.5에서는 future 문이 있을 때만 새 문법이 인식됩니다.:
from __future__ import with_statement
이렇게 하면 ‘with’와 ‘as’가 모두 키워드가 됩니다. future 문 없이 ‘with’나 ‘as’를 식별자로 사용하면 stderr로 Warning이 출력됩니다.
Python 2.6에서는 새 문법이 항상 인식되며, ‘with’와 ‘as’는 항상 키워드입니다.
제너레이터 데코레이터
PEP 342가 수락되었으므로, 정확히 한 번 yield하는 제너레이터를 사용하여 with 문을 제어할 수 있게 해 주는 데코레이터를 작성할 수 있습니다. 다음은 그러한 데코레이터의 개략적인 구현입니다:
class GeneratorContextManager(object):
def __init__(self, gen):
self.gen = gen
def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")
def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But
# throw() has to raise the exception to signal
# propagation, so this fixes the impedance mismatch
# between the throw() protocol and the __exit__()
# protocol.
#
if sys.exc_info()[1] is not value:
raise
def contextmanager(func):
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper
이 데코레이터는 다음과 같이 사용할 수 있습니다:
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here (however unlikely)
이 데코레이터의 견고한 구현이 표준 라이브러리의 일부로 포함될 것입니다.
표준 라이브러리의 컨텍스트 관리자
파일, 소켓, 잠금과 같은 특정 객체에 __enter__()와 __exit__() 메서드를 부여하여, 다음과 같이 작성하는 대신:
with locking(myLock):
BLOCK
간단히 다음과 같이 작성할 수 있게 하는 것도 가능할 것입니다:
with myLock:
BLOCK
저는 이 부분에 신중해야 한다고 생각합니다. 다음과 같은 실수로 이어질 수 있기 때문입니다:
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2
이 코드는 생각하는 대로 동작하지 않습니다(BLOCK2에 진입하기 전에 f가 닫힙니다).
반면에 이러한 실수는 쉽게 진단할 수 있습니다. 예를 들어, 위의 제너레이터 컨텍스트 데코레이터는 두 번째 with 문이 f.__enter__()를 다시 호출하면 RuntimeError를 발생시킵니다. 닫힌 파일 객체에 대해 __enter__가 호출될 때에도 비슷한 오류를 발생시킬 수 있습니다.
Python 2.5에서는 다음 타입들이 컨텍스트 관리자로 확인되었습니다:
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
또한 decimal 모듈에도 컨텍스트 관리자가 추가되어, with 문의 본문 안에서 지역적인 decimal 산술 컨텍스트를 사용할 수 있게 하고 with 문을 빠져나갈 때 원래 컨텍스트를 자동으로 복원합니다.
표준 용어
이 PEP는 __enter__()와 __exit__() 메서드로 구성된 프로토콜을 “컨텍스트 관리 프로토콜”이라 부르고, 그 프로토콜을 구현하는 객체를 “컨텍스트 관리자”라 부를 것을 제안합니다. [4]
문에서 with 키워드 바로 뒤에 오는 표현식은 “컨텍스트 표현식”입니다. 이 표현식이 문 본문이 실행되는 동안 컨텍스트 관리자가 설정하는 런타임 환경에 대한 주된 단서를 제공하기 때문입니다.
with 문의 본문에 있는 코드와 as 키워드 뒤의 변수 이름(또는 이름들)에는 현재로서는 딱히 특별한 용어가 없습니다. “문 본문(statement body)”과 “대상 목록(target list)”이라는 일반 용어를 사용할 수 있으며, 그렇게 해서는 용어가 불분명할 경우 앞에 “with”나 “with 문”을 붙일 수 있습니다.
decimal 모듈의 산술 컨텍스트와 같은 객체가 존재하므로, 안타깝게도 “컨텍스트”라는 용어는 모호합니다. 필요하다면 컨텍스트 표현식이 생성하는 구체적인 객체에는 “컨텍스트 관리자”라는 용어를, 컨텍스트 관리자가 실제로 수행하는 상태 변경에는 “런타임 컨텍스트” 또는 (가급적) “런타임 환경”이라는 용어를 사용하여 더 구체적으로 표현할 수 있습니다. 단순히 with 문의 사용법을 논의할 때는 컨텍스트 표현식이 런타임 환경에 가해지는 변경을 완전히 정의하므로, 이 모호함이 크게 문제 되지 않을 것입니다. 이 구분은 with 문 자체의 동작 방식과 컨텍스트 관리자를 실제로 구현하는 방법을 논의할 때 더 중요합니다.
컨텍스트 관리자 캐싱
많은 컨텍스트 관리자(파일, 제너레이터 기반 컨텍스트 등)는 일회용 객체입니다. __exit__() 메서드가 한 번 호출되고 나면 컨텍스트 관리자는 더 이상 사용 가능한 상태가 아닙니다(예: 파일이 닫혔거나, 내부 제너레이터의 실행이 끝났습니다).
with 문마다 새 관리자 객체를 요구하는 것이, 멀티스레드 코드와 중첩된 with 문이 같은 컨텍스트 관리자를 사용하려 할 때 생기는 문제를 피하는 가장 쉬운 방법입니다. 재사용을 지원하는 표준 라이브러리 컨텍스트 관리자가 모두 threading 모듈에서 나온 것은 우연이 아닙니다. 이들은 모두 스레드 사용과 중첩 사용으로 인해 생기는 문제를 처리하도록 이미 설계되어 있습니다.
즉, 특정 초기화 인자를 준 컨텍스트 관리자를 저장해 두고 여러 with 문에서 사용하려면, 일반적으로 컨텍스트 관리자를 직접 캐싱하는 대신 인자가 없는 호출 가능 객체에 저장해 두고 각 문의 컨텍스트 표현식에서 그 호출 가능 객체를 호출해야 합니다.
이 제약이 적용되지 않는 컨텍스트 관리자라면 해당 문서에 그 사실을 명확히 밝혀야 합니다.
해결된 문제
다음 쟁점들은 BDFL의 승인으로 해결되었습니다(python-dev에서도 큰 반대가 없었습니다).
- 내부의 제너레이터-이터레이터가 오작동할 때
GeneratorContextManager는 어떤 예외를 발생시켜야 합니까? 다음 인용문은 Guido가 이 경우와 PEP 342의 제너레이터close()메서드 모두에RuntimeError를 선택한 이유입니다(출처: [8]):“단지 이 목적만을 위해 새 예외 클래스를 도입하고 싶지는 않습니다. 사람들이 잡기를 바라는 예외가 아니기 때문입니다. 저는 이 예외가 트레이스백으로 이어지고, 프로그래머가 그것을 보고 코드를 고치기를 바랍니다. 그래서 이제는 두 경우 모두
RuntimeError를 발생시켜야 한다고 생각합니다. 이에 대한 선례가 몇 가지 있습니다. 무한 재귀가 감지되는 상황에서, 그리고 초기화되지 않은 객체에 대해(또한 기타 다양한 조건에서) 코어 Python 코드가 이 예외를 발생시킵니다.” - with 문에 관여하는 클래스에 해당 메서드가 없을 때
TypeError대신AttributeError를 발생시켜도 괜찮습니다. 추상 객체 C API가AttributeError가 아니라TypeError를 발생시키는 것은 의도적인 설계 결정이 아니라 역사적 우연입니다 [11]. __enter__/__exit__메서드를 가진 객체를 “컨텍스트 관리자”라고 부르며, 제너레이터 함수를 컨텍스트 관리자 팩토리로 변환하는 데코레이터는contextlib.contextmanager입니다. 2.5 릴리스 주기 동안 다른 제안들도 몇 가지 있었지만 [15], PEP 구현에서 사용되어 온 용어에서 벗어나야 할 설득력 있는 인자(논거)는 제시되지 않았습니다.
기각된 선택지
몇 달 동안 이 PEP는 숨겨진 흐름 제어를 피하기 위해 예외 억제를 금지했습니다. 그러나 구현해 보니 이것이 엄청난 골칫거리임이 드러났고, 그래서 Guido가 이 기능을 되살렸습니다 [12].
이 PEP에서 끝없는 질문과 용어 논쟁을 불러일으킨 또 다른 측면은 이터러블의 __iter__() 메서드와 유사한 __context__() 메서드를 제공하는 것이었습니다 [5] [7] [9]. 그것이 무엇이고, 왜 존재하며, 어떻게 동작하도록 의도되었는지를 설명하는 데 계속 문제가 생겼고 [10] [12], 결국 Guido가 이 개념을 완전히 폐기했습니다 [14] (그리고 모두가 크게 기뻐했습니다!).
PEP 342 제너레이터 API를 직접 사용해 with 문을 정의하자는 생각도 잠시 검토되었지만 [6], 제너레이터 기반이 아닌 컨텍스트 관리자를 작성하기가 너무 어려워진다는 이유로 곧 기각되었습니다.
예시
제너레이터 기반 예제는 PEP 342에 의존합니다. 또한 일부 예제는 실제로는 필요하지 않습니다. threading.RLock 같은 적절한 객체는 with 문에서 직접 사용할 수 있기 때문입니다.
예제 컨텍스트의 이름에 사용된 시제는 임의로 정한 것이 아닙니다. 이름이 __enter__ 메서드에서 수행되고 __exit__ 메서드에서 되돌려지는 동작을 가리킬 때는 과거형(“-ed”)을 사용합니다. 이름이 __exit__ 메서드에서 수행될 동작을 가리킬 때는 진행형(“-ing”)을 사용합니다.
- 블록 시작 시 획득한 잠금이 블록을 벗어날 때 해제되도록 보장하는 템플릿:
@contextmanager def locked(lock): lock.acquire() try: yield finally: lock.release()
다음과 같이 사용합니다:
with locked(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).
- 파일을 열고, 블록을 벗어날 때 파일이 닫히도록 보장하는 템플릿:
@contextmanager def opened(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
다음과 같이 사용합니다:
with opened("/etc/passwd") as f: for line in f: print line.rstrip()
- 데이터베이스 트랜잭션을 커밋하거나 롤백하는 템플릿:
@contextmanager def transaction(db): db.begin() try: yield None except: db.rollback() raise else: db.commit()
- 제너레이터 없이 다시 작성한 예제 1:
class locked: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire() def __exit__(self, type, value, tb): self.lock.release()
(이 예제는 상태가 거의 없는 다른 예제들도 구현할 수 있도록 쉽게 수정할 수 있습니다. 즉, 특별히 보존해야 할 상태가 없다면 제너레이터 없이도 쉽게 구현할 수 있음을 보여 줍니다.)
- stdout을 일시적으로 리디렉션합니다:
@contextmanager def stdout_redirected(new_stdout): save_stdout = sys.stdout sys.stdout = new_stdout try: yield None finally: sys.stdout = save_stdout
다음과 같이 사용합니다:
with opened(filename, "w") as f: with stdout_redirected(f): print "Hello world"
물론 이것은 스레드 안전하지 않지만, 같은 작업을 수동으로 하는 것도 스레드 안전하지 않기는 마찬가지입니다. 단일 스레드 프로그램(예를 들어 스크립트)에서는 이 방식이 널리 쓰입니다.
- 오류 상태도 함께 반환하는
opened()의 변형:@contextmanager def opened_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
다음과 같이 사용합니다:
with opened_w_error("/etc/passwd", "a") as (f, err): if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
- 또 다른 유용한 예로 시그널을 차단하는 작업을 들 수 있습니다. 다음과 같이 사용할 수 있습니다:
import signal with signal.blocked(): # code executed without worrying about signals
선택적 인자로 차단할 시그널의 리스트를 받을 수 있으며, 기본적으로는 모든 시그널을 차단합니다. 구현은 독자의 연습 문제로 남겨 둡니다.
- 이 기능의 또 다른 용도는 Decimal 컨텍스트입니다. 다음은 Michael Chermside가 게시한 예를 본뜬 간단한 예입니다:
import decimal @contextmanager def extra_precision(places=2): c = decimal.getcontext() saved_prec = c.prec c.prec += places try: yield None finally: c.prec = saved_prec
사용 예(Python 라이브러리 레퍼런스의 예를 수정함):
def sin(x): "Return the sine of x as measured in radians." with extra_precision(): i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1 while s != lasts: lasts = s i += 2 fact *= i * (i-1) num *= x * x sign *= -1 s += num / fact * sign # The "+s" rounds back to the original precision, # so this must be outside the with-statement: return +s
- 다음은 decimal 모듈을 위한 간단한 컨텍스트 관리자입니다:
@contextmanager def localcontext(ctx=None): """Set a new local decimal context for the block""" # Default to using the current context if ctx is None: ctx = getcontext() # We set the thread context to a copy of this context # to ensure that changes within the block are kept # local to the block. newctx = ctx.copy() oldctx = decimal.getcontext() decimal.setcontext(newctx) try: yield newctx finally: # Always restore the original context decimal.setcontext(oldctx)
사용 예:
from decimal import localcontext, ExtendedContext def sin(x): with localcontext() as ctx: ctx.prec += 2 # Rest of sin calculation algorithm # uses a precision 2 greater than normal return +s # Convert result to normal precision def sin(x): with localcontext(ExtendedContext): # Rest of sin calculation algorithm # uses the Extended Context from the # General Decimal Arithmetic Specification return +s # Convert result to normal context
- 범용 “객체 닫기” 컨텍스트 관리자:
class closing(object): def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj def __exit__(self, *exc_info): try: close_it = self.obj.close except AttributeError: pass else: close_it()
이것은 파일이든 제너레이터든 그 밖의 무엇이든, close 메서드가 있는 것이라면 무엇이든 결정적으로 닫는 데 사용할 수 있습니다. 객체를 반드시 닫아야 한다는 보장이 없는 경우(예: 임의의 이터러블을 받는 함수)에도 사용할 수 있습니다:
# emulate opening(): with closing(open("argument.txt")) as contradiction: for line in contradiction: print line # deterministically finalize an iterator: with closing(iter(data_source)) as data: for datum in data: process(datum)
(Python 2.5의 contextlib 모듈에는 이 컨텍스트 관리자의 한 버전이 포함되어 있습니다)
- PEP 319는 이전에 획득한 잠금을 일시적으로 해제하는
released()컨텍스트도 함께 두는 사용 사례를 제시합니다. 이것은 위의 locked 컨텍스트 관리자에서acquire()와release()호출을 맞바꾸기만 하면 매우 비슷하게 작성할 수 있습니다:class released: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.release() def __exit__(self, type, value, tb): self.lock.acquire()
사용 예:
with my_lock: # Operations with the lock held with released(my_lock): # Operations without the lock # e.g. blocking I/O # Lock is held again here
- 과도한 들여쓰기를 피하기 위해 주어진 컨텍스트들을 왼쪽에서 오른쪽으로 자동으로 중첩하는 “중첩” 컨텍스트 관리자:
@contextmanager def nested(*contexts): exits = [] vars = [] try: try: for context in contexts: exit = context.__exit__ enter = context.__enter__ vars.append(enter()) exits.append(exit) yield vars except: exc = sys.exc_info() else: exc = (None, None, None) finally: while exits: exit = exits.pop() try: exit(*exc) except: exc = sys.exc_info() else: exc = (None, None, None) if exc != (None, None, None): # sys.exc_info() may have been # changed by one of the exit methods # so provide explicit exception info raise exc[0], exc[1], exc[2]
사용 예:
with nested(a, b, c) as (x, y, z): # Perform operation
다음과 동등합니다:
with a as x: with b as y: with c as z: # Perform operation
(Python 2.5의 contextlib 모듈에는 이 컨텍스트 관리자의 한 버전이 포함되어 있습니다)
참조 구현
이 PEP는 2005년 6월 27일 EuroPython 기조연설에서 Guido가 처음 수락했습니다. 이후 the __context__ 메서드가 추가된 상태로 다시 수락되었습니다. 이 PEP는 Python 2.5a1을 위해 Subversion에 구현되었습니다. __context__() 메서드는 Python 2.5b1에서 제거되었습니다
감사의 글
PEP 340과 PEP 346의 감사의 글에 언급된 모든 분을 포함하여 많은 분이 이 PEP의 아이디어와 개념에 기여했습니다.
다음 분들께도 감사드립니다(순서에는 특별한 의미가 없습니다): Paul Moore, Phillip J. Eby, Greg Ewing, Jason Orendorff, Michael Hudson, Raymond Hettinger, Walter Dörwald, Aahz, Georg Brandl, Terry Reedy, A.M. Kuchling, Brett Cannon, 그리고 python-dev 토론에 참여해 주신 모든 분들.
참고 문헌
Copyright
This document has been placed in the public domain.