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 베타 릴리스 무렵에 안정화되었습니다.
맞습니다. 몇몇 부분에서는 동사 시제가 엉켜 있습니다. 이제 1년 넘게 이 PEP를 작업해 왔으므로, 원래 미래에 속했던 일들이 이제는 과거에 속하게 되었습니다 :)
서론
관련 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 Wiki [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 예외로 인해 두 가상 머신 opcode 사이에서 종료될 수 있습니다.)
이 주장은 제가 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을 발생시킬 것으로 예상합니다. (자세한 내용은 아래의 Optional Generator Decorator 절을 참조하십시오.)
이제 마지막 장애물은 PEP 310의 구문이 다음과 같다는 점이었습니다.:
with VAR = EXPR:
BLOCK1
이는 오해를 불러일으킬 수 있습니다. VAR가 EXPR의 값을 받지 않기 때문입니다. 관련 PEP 340에서 아이디어를 빌려, 다음 단계로 나아가기는 쉬웠습니다:
with EXPR as VAR:
BLOCK1
추가 논의를 통해 사람들은 예외를 기록하기 위한 것뿐이라 하더라도 제너레이터 안에서 예외를 “볼” 수 있다는 점을 매우 좋아한다는 사실이 드러났습니다. with 문은 루프처럼 사용할 수 없어야 하므로 제너레이터는 다른 값을 yield할 수 없습니다(다른 예외를 발생시키는 것은 근소하게 허용할 만합니다). 이를 가능하게 하기 위해 제너레이터에 새로운 throw() 메서드를 제안합니다. 이 메서드는 일반적인 방식(type, value, traceback)으로 예외를 나타내는 1~3개의 인자를 받고, 제너레이터가 일시 중단된 지점에서 해당 예외를 발생시킵니다.
이것이 가능해지면, 특수한 예외인 GeneratorExit를 인자로 throw()를 호출하는 또 다른 제너레이터 메서드 close()를 제안하는 것은 작은 단계에 불과합니다. 이는 제너레이터에 종료하라고 알리며, 그다음에는 제너레이터가 가비지 컬렉션될 때 close()가 자동으로 호출되도록 제안하는 것도 작은 단계에 불과합니다.
그러면 마지막으로 try-finally 문 안에서 yield 문을 허용할 수 있습니다. 이제 finally 절이 (결국에는) 실행된다는 것을 보장할 수 있기 때문입니다. 종료 처리에 관한 일반적인 주의 사항이 적용됩니다. 즉, 어떤 객체도 종료 처리하지 않은 채 프로세스가 갑자기 종료될 수 있으며, 애플리케이션의 순환 참조나 메모리 누수로 인해 객체가 영원히 살아 있을 수 있습니다(Python 구현의 순환 참조나 누수는 GC가 처리합니다).
제너레이터 객체가 사용되지 않게 된 직후에 finally 절이 실행된다는 것을 보장하는 것은 아니라는 점에 유의하십시오. CPython에서는 실제로 이렇게 작동하지만 말입니다. 이는 파일의 자동 닫기와 비슷합니다. CPython과 같은 참조 카운팅 구현은 객체에 대한 마지막 참조가 사라지는 즉시 객체를 할당 해제하지만, 다른 GC 알고리즘을 사용하는 구현은 동일한 보장을 하지 않습니다. 이는 Jython, IronPython, 그리고 아마도 Parrot에서 실행되는 Python에도 적용됩니다.
(제너레이터에 적용된 변경 사항의 자세한 내용은 이제 현재 PEP가 아니라 PEP 342에서 확인할 수 있습니다.)
사용 사례
끝부분 근처의 Examples 절을 참조하십시오.
사양: ‘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__()의 호출 규약은 다음과 같습니다. finally 절에 BLOCK의 정상적인 완료를 통해 도달했거나 비지역 goto(BLOCK의 break, continue 또는 return 문)를 통해 도달한 경우, mgr.__exit__()는 세 개의 None인자로 호출됩니다. BLOCK에서 발생한 예외를 통해 finally 절에 도달한 경우, mgr.__exit__()는 예외 유형, 값 및 트레이스백을 나타내는 세 인자로 호출됩니다.
중요: mgr.__exit__()가 “true” 값을 반환하면 예외가 “삼켜집니다”. 즉, “true”를 반환하면 with 문 내부에서 예외가 발생했더라도 with 문 다음의 다음 문에서 실행이 계속됩니다. 그러나 with 문이 비지역 goto(break, continue 또는 return)를 통해 종료된 경우에는 반환 값과 관계없이 mgr.__exit__()가 반환할 때 이 비지역 반환이 재개됩니다. 이 세부 사항의 목적은 mgr.__exit__()가 예외를 삼킬 수 있게 하면서도 이를 지나치게 쉽게 만들지 않는 것입니다(기본 반환 값인 None은 거짓이며, 이로 인해 예외가 다시 발생하기 때문입니다). 예외를 삼키는 주요 사용 사례는 @contextmanager 데코레이터를 작성할 수 있게 하는 것입니다. 그러면 데코레이터가 적용된 제너레이터의 try/except 블록이 제너레이터 본문이 with 문이 있는 위치에 인라인으로 확장된 경우와 정확히 동일하게 동작합니다.
인자 없는 __exit__()를 사용하는 PEP 310의 방식과 달리 예외 세부 정보를 __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가 승인되면서, 정확히 한 번 값을 산출하는 제너레이터를 사용하여 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
with 문의 본문에서 로컬 decimal 산술 컨텍스트를 사용할 수 있도록 decimal 모듈에도 컨텍스트 관리자가 추가되며, with 문을 종료할 때 원래 컨텍스트가 자동으로 복원됩니다.
표준 용어
이 PEP에서는 __enter__()와 __exit__() 메서드로 구성된 프로토콜을 “컨텍스트 관리 프로토콜”이라고 하고, 해당 프로토콜을 구현하는 객체를 “컨텍스트 관리자”라고 부를 것을 제안합니다. [4]
문에서 with 키워드 바로 뒤에 오는 표현식은 “컨텍스트 표현식”이라고 합니다. 이 표현식이 문 본문이 실행되는 동안 컨텍스트 관리자가 설정하는 런타임 환경에 대한 주요 단서를 제공하기 때문입니다.
현재로서는 with 문의 본문에 있는 코드와 as 키워드 뒤의 변수 이름(또는 이름들)에 특별한 용어가 실제로 정해져 있지 않습니다. 일반적인 용어인 “문 본문”과 “대상 리스트”를 사용할 수 있으며, 그렇지 않으면 용어가 명확하지 않은 경우 앞에 “with” 또는 “with 문”을 붙이면 됩니다.
decimal 모듈의 산술 컨텍스트와 같은 객체가 존재하므로 “컨텍스트”라는 용어는 안타깝게도 모호합니다. 필요한 경우 컨텍스트 표현식이 생성하는 구체적인 객체에는 “컨텍스트 관리자”라는 용어를 사용하고, 컨텍스트 관리자가 수행하는 실제 상태 변경에는 “런타임 컨텍스트” 또는 (가급적) “런타임 환경”이라는 용어를 사용하여 더 구체적으로 표현할 수 있습니다. with 문의 사용법만 간단히 논의할 때에는 모호성이 크게 문제가 되지 않습니다. 컨텍스트 표현식이 런타임 환경에 적용되는 변경 사항을 완전히 정의하기 때문입니다. 이러한 구분은 with 문 자체의 작동 방식과 컨텍스트 관리자를 실제로 구현하는 방법을 논의할 때 더욱 중요합니다.
컨텍스트 관리자 캐싱
많은 컨텍스트 관리자(파일 및 제너레이터 기반 컨텍스트 등)는 한 번만 사용할 수 있는 객체가 됩니다. __exit__() 메서드가 호출되면 컨텍스트 관리자는 더 이상 사용할 수 있는 상태가 아닙니다(예: 파일이 닫혔거나 기본 제너레이터의 실행이 완료된 경우).
각 with 문마다 새로운 관리자 객체를 요구하는 것이 멀티스레드 코드와 동일한 컨텍스트 관리자를 사용하려는 중첩된 with 문으로 인해 발생하는 문제를 피하는 가장 쉬운 방법입니다. 재사용을 지원하는 표준 라이브러리의 모든 컨텍스트 관리자가 threading 모듈에서 제공되는 것은 우연이 아닙니다. 이들은 모두 스레드 및 중첩 사용으로 인해 발생하는 문제를 처리하도록 이미 설계되어 있습니다.
이는 특정 초기화 인자를 사용하여 여러 with 문에서 사용할 컨텍스트 관리자를 저장하려면, 일반적으로 컨텍스트 관리자를 직접 캐시하는 대신 각 문의 컨텍스트 표현식에서 호출할 0인자 호출 가능 객체에 저장해야 한다는 의미입니다.
이 제한이 적용되지 않는 경우에는 해당 컨텍스트 관리자의 문서에서 이를 명확히 밝혀야 합니다.
해결된 문제
다음 문제들은 BDFL의 승인과 python-dev에서 중대한 반대 의견이 없었던 덕분에 해결되었습니다.
- 기저 제너레이터-이터레이터가 잘못 동작할 때
GeneratorContextManager는 어떤 예외를 발생시켜야 합니까? 다음 인용문은 이 경우와 PEP 342의 제너레이터close()메서드 모두에RuntimeError를 선택한 Guido의 결정 배경입니다([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()
- 제너레이터 없이 다시 작성한 예제:
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()컨텍스트도 사용하는 사례를 제시합니다. 이는 위의 잠금 컨텍스트 관리자에서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의 아이디어와 개념에는 PEP 340 및 PEP 346의 감사의 글에 언급된 모든 분을 포함하여 많은 분이 기여했습니다.
추가로 다음 분들께 감사드립니다(특별한 순서는 아닙니다): 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.