PEP 3134 – 예외 연결 및 내장 트레이스백
- Author:
- Ka-Ping Yee
- Status:
- Final
- Type:
- Standards Track
- Created:
- 12-May-2005
- Python-Version:
- 3.0
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
번호 매기기 참고
이 PEP는 PEP 344로 시작했습니다. 이제 Python 3000을 대상으로 하므로 3xxx 영역으로 옮겨졌습니다.
초록
이 PEP는 예외 인스턴스에 세 가지 표준 속성을 제안합니다. 암시적으로 연결된 예외를 위한 __context__ 속성, 명시적으로 연결된 예외를 위한 __cause__ 속성, 그리고 트레이스백을 위한 __traceback__ 속성입니다. 새로운 raise ... from 문은 __cause__ 속성을 설정합니다.
동기
하나의 예외(예외 A)를 처리하는 동안 또 다른 예외(예외 B)가 발생할 수 있습니다. 현재 Python(버전 2.4)에서는 이런 일이 발생하면 예외 B가 외부로 전파되고 예외 A는 사라집니다. 문제를 디버깅하려면 두 예외를 모두 아는 것이 유용합니다. __context__ 속성은 이 정보를 자동으로 보존합니다.
때로는 예외 처리기가 추가 정보를 제공하거나 예외를 다른 유형으로 변환하기 위해 의도적으로 예외를 다시 발생시키는 것이 유용할 수 있습니다. __cause__ 속성은 예외의 직접적인 원인을 명시적으로 기록하는 방법을 제공합니다.
현재 Python 구현에서 예외는 유형, 값, 트레이스백이라는 세 부분으로 구성됩니다. sys 모듈은 현재 예외를 exc_type, exc_value, exc_traceback이라는 세 개의 병렬 변수로 노출하며, sys.exc_info() 함수는 이 세 부분의 튜플을 반환하고, raise 문에는 이 세 부분을 받는 세 인자 형식이 있습니다. 예외를 조작하려면 이 세 가지를 병렬로 전달해야 하는 경우가 많으며, 이는 번거롭고 오류가 발생하기 쉽습니다. 또한 except 문은 트레이스백이 아니라 값에만 접근할 수 있습니다. 예외 값에 __traceback__ 속성을 추가하면 모든 예외 정보에 한 곳에서 접근할 수 있습니다.
역사
Raymond Hettinger [1]는 2003년 1월 Python-Dev에서 마스킹된 예외 문제를 제기하고, C 모듈이 현재 활성화된 예외에 더 많은 정보를 추가하는 데 사용할 수 있는 PyErr_FormatAppend() 함수를 제안했습니다. Brett Cannon [2]은 2003년 6월에 연결된 예외를 다시 제기했고, 이로 인해 긴 논의가 이어졌습니다.
Greg Ewing [3]은 원래 예외로 인해 시작된 해제 과정 중 finally 블록에서 예외가 발생하는 경우와, 원래 예외를 처리하는 except 블록에서 예외가 발생하는 경우가 서로 다르다는 점을 지적했습니다.
Greg Ewing [4] 및 Guido van Rossum [5], 그리고 아마 다른 사람들도 이전에 Exception 인스턴스에 트레이스백 속성을 추가하는 것을 언급했습니다. 이는 PEP 3000에 기록되어 있습니다.
이 PEP는 최근 Python-Dev에 같은 아이디어가 다시 게시된 또 다른 사건 [6] [7]에서 동기를 얻었습니다.
근거
Python-Dev의 논의에서는 서로 상당히 다른 두 가지 목적을 위한 예외 연결에 관심이 있음이 드러났습니다. 보조 예외가 예기치 않게 발생하는 상황을 처리하려면 해당 예외를 암시적으로 보존해야 합니다. 예외를 의도적으로 변환하려면 예외를 명시적으로 연결하는 방법이 있어야 합니다. 이 PEP는 두 가지 목적을 모두 다룹니다.
연결된 예외의 여러 속성 이름이 Python-Dev [2]에서 제안되었으며, cause, antecedent, reason, original, chain, chainedexc, exc_chain, excprev, previous, precursor 등이 포함됩니다. 명시적으로 연결된 예외의 경우, 이 PEP는 그 특정한 의미 때문에 __cause__를 제안합니다. 암시적으로 연결된 예외의 경우, 의도된 의미가 시간적 선행 관계보다는 구체적이지만 인과 관계보다는 덜 구체적이기 때문에 이 PEP는 __context__라는 이름을 제안합니다. 즉, 다른 예외를 처리하는 맥락에서 예외가 발생합니다.
이 세 속성에 앞뒤로 이중 밑줄이 붙은 이름을 사용할 것을 이 PEP가 제안하는 이유는 해당 속성들이 Python VM에 의해 설정되기 때문입니다. 매우 특수한 경우에만 일반 할당으로 이 속성들을 설정해야 합니다.
이 PEP는 except 블록과 finally 블록에서 발생하는 예외를 동일한 방식으로 처리합니다. 트레이스백을 읽으면 예외가 어디에서 발생했는지 명확하므로, 두 경우를 구별하기 위한 추가 메커니즘은 불필요한 복잡성만 더하게 됩니다.
이 PEP는 가장 바깥쪽 예외 객체(except 절에서 매칭에 사용되는 객체)가 현재 동작과의 하위 호환성을 위해 가장 최근에 발생한 예외가 되도록 제안합니다.
이 PEP는 트레이스백이 가장 바깥쪽 예외를 마지막에 표시하도록 제안합니다. 이는 트레이스백의 시간순서(가장 오래된 프레임부터 가장 최근 프레임까지)와 일관되며, 실제로 발생한 예외를 마지막 줄에서 더 쉽게 찾을 수 있기 때문입니다.
단순성을 유지하기 위해, 예외를 설정하는 C API 호출은 예외의 __context__를 자동으로 설정하지 않습니다. Guido van Rossum은 이러한 변경을 적용하는 것에 우려를 표명했습니다 [8].
다른 언어의 경우, Java와 Ruby는 catch/rescue 또는 finally/ensure 절에서 다른 예외가 발생하면 원래 예외를 모두 버립니다. Perl 5에는 기본 제공 구조적 예외 처리가 없습니다. Perl 6의 경우, RFC 번호 88 [9]은 연결된 예외를 @@라는 이름의 배열에 암시적으로 보존하는 예외 메커니즘을 제안합니다. 해당 RFC에서는 이 PEP와 마찬가지로 가장 최근에 발생한 예외가 매칭에 사용되도록 노출되며, 예외 매칭을 위해 임의의 표현식(@@를 포함할 수도 있음)을 평가할 수 있습니다.
C#의 예외에는 다른 예외를 가리킬 수 있는 읽기 전용 InnerException속성이 포함되어 있습니다. 해당 문서 [10]에는 “예외 X가 이전 예외 Y의 직접적인 결과로 발생한 경우, X의 InnerException속성에는 Y에 대한 참조가 포함되어야 합니다.”라고 설명되어 있습니다. 이 속성은 VM에 의해 자동으로 설정되지 않습니다. 대신 모든 예외 생성자는 명시적으로 설정할 수 있도록 선택적 innerException 인자를 받습니다. __cause__ 속성은 InnerException과 동일한 목적을 수행하지만, 이 PEP는 모든 예외의 생성자를 확장하는 대신 새로운 형태의 raise를 제안합니다. C#은 InnerException 체인의 끝으로 직접 이동하는 GetBaseException 메서드도 제공하지만, 이 PEP는 이에 상응하는 기능을 제안하지 않습니다.
이 세 속성을 하나의 제안에서 함께 제시하는 이유는 __traceback__ 속성이 연결된 예외의 트레이스백에 편리하게 접근할 수 있도록 하기 때문입니다.
암시적 예외 연결
다음은 __context__ 속성을 설명하는 예입니다.:
def compute(a, b):
try:
a/b
except Exception, exc:
log(exc)
def log(exc):
file = open('logfile.txt') # oops, forgot the 'w'
print >>file, exc
file.close()
compute(0, 0)을 호출하면 ZeroDivisionError가 발생합니다. compute() 함수는 이 예외를 포착하고 log(exc)를 호출하지만, 쓰기용으로 열리지 않은 파일에 쓰려고 할 때 log() 함수에서도 예외가 발생합니다.
현재 Python에서는 compute()의 호출자에게 IOError가 발생합니다. ZeroDivisionError는 사라집니다. 제안된 변경 사항을 적용하면 IOError의 인스턴스에 ZeroDivisionError를 보존하는 추가 __context__ 속성이 생깁니다.
다음의 더 복잡한 예는 finally 절과 except 절이 혼합된 경우의 처리를 보여 줍니다.:
def main(filename):
file = open(filename) # oops, forgot the 'w'
try:
try:
compute()
except Exception, exc:
log(file, exc)
finally:
file.clos() # oops, misspelled 'close'
def compute():
1/0
def log(file, exc):
try:
print >>file, exc # oops, file is not writable
except:
display(exc)
def display(exc):
print ex # oops, misspelled 'exc'
기존 파일의 이름을 사용하여 main()을 호출하면 네 개의 예외가 발생합니다. 최종 결과는 clos의 철자 오류로 인해 발생한 AttributeError이며, 해당 예외의 __context__는 ex의 철자 오류로 인해 발생한 NameError를 가리키고, 해당 예외의 __context__는 파일이 읽기 전용이기 때문에 발생한 IOError를 가리키며, 해당 예외의 __context__는 ZeroDivisionError를 가리키고, 해당 예외의 __context__속성은 None입니다.
제안된 의미 체계는 다음과 같습니다.
- 각 스레드에는 처음에
None으로 설정된 예외 컨텍스트가 있습니다. - 예외가 발생할 때마다 예외 인스턴스에 이미
__context__속성이 없다면, 인터프리터는 해당 속성을 스레드의 예외 컨텍스트와 같게 설정합니다. - 예외가 발생한 직후 스레드의 예외 컨텍스트가 해당 예외로 설정됩니다.
- 인터프리터가 끝에 도달하거나
return,yield,continue,break문을 실행하여except블록을 벗어날 때마다, 스레드의 예외 컨텍스트가None으로 설정됩니다.
명시적 예외 연결
예외 객체의 __cause__ 속성은 항상 None으로 초기화됩니다. 이는 새로운 형식의 raise 문에 의해 설정됩니다.:
raise EXCEPTION from CAUSE
이는 다음과 동일합니다.:
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
다음 예에서는 데이터베이스가 몇 가지 서로 다른 종류의 저장소에 대한 구현을 제공하며, 파일 저장소도 그중 하나입니다. 데이터베이스 설계자는 클라이언트가 저장소별 세부 사항을 알 필요가 없도록 오류가 DatabaseError 객체로 전파되기를 원하지만, 기본 오류 정보가 손실되는 것은 원하지 않습니다.
class DatabaseError(Exception):
pass
class FileDatabase(Database):
def __init__(self, filename):
try:
self.file = open(filename)
except IOError, exc:
raise DatabaseError('failed to open') from exc
open() 호출에서 예외가 발생하면 해당 문제는 DatabaseError로 보고되며, __cause__속성을 통해 IOError가 원래 원인임을 알 수 있습니다.
트레이스백 속성
다음 예에서는 __traceback__ 속성을 보여 줍니다.
def do_logged(file, work):
try:
work()
except Exception, exc:
write_exception(file, exc)
raise exc
from traceback import format_tb
def write_exception(file, exc):
...
type = exc.__class__
message = str(exc)
lines = format_tb(exc.__traceback__)
file.write(... type ... message ... lines ...)
...
현재 Python에서는 do_logged() 함수가 sys.exc_traceback 또는 sys.exc_info() [2]에서 트레이스백을 추출한 다음 값과 트레이스백을 모두 write_exception()에 전달해야 합니다. 제안된 변경 사항을 적용하면 write_exception()은 인자 하나만 받아 __traceback__속성을 사용하여 예외를 가져옵니다.
제안된 의미 체계는 다음과 같습니다.
- 예외가 포착될 때마다 예외 인스턴스에 이미
__traceback__속성이 없다면, 인터프리터는 해당 속성을 새로 포착된 트레이스백으로 설정합니다.
향상된 보고
기본 예외 처리기는 연결된 예외를 보고하도록 수정됩니다. 예외 연결은 __cause__ 및 __context__ 속성을 따라 순회하며, __cause__가 우선됩니다. 트레이스백의 시간순서에 따라 가장 최근에 발생한 예외가 마지막에 표시됩니다. 즉, 표시는 가장 안쪽 예외에 대한 설명으로 시작하여 연결을 따라 가장 바깥쪽 예외까지 거슬러 올라갑니다. 트레이스백은 평소와 같이 형식이 지정되며, 다음 행 중 하나가:
The above exception was the direct cause of the following exception:
또는
During handling of the above exception, another exception occurred:
__cause__로 연결되었는지 __context__로 연결되었는지에 따라 트레이스백 사이에 표시됩니다. 절차의 개략은 다음과 같습니다.:
def print_chain(exc):
if exc.__cause__:
print_chain(exc.__cause__)
print '\nThe above exception was the direct cause...'
elif exc.__context__:
print_chain(exc.__context__)
print '\nDuring handling of the above exception, ...'
print_exc(exc)
traceback 모듈의 format_exception, print_exception, print_exc, print_last 함수는 기본값이 True인 선택적 chain 인자를 받도록 업데이트됩니다. 이 인자가 True인 경우 이러한 함수는 방금 설명한 대로 예외의 전체 연결을 형식 지정하거나 표시합니다. 이 인자가 False인 경우 이러한 함수는 가장 바깥쪽 예외만 형식 지정하거나 표시합니다.
cgitb 모듈도 예외의 전체 연결을 표시하도록 업데이트해야 합니다.
C API
예외를 설정하는 PyErr_Set* 호출은 예외의 __context__ 속성을 설정하지 않습니다. PyErr_NormalizeException은 항상 traceback 속성을 해당 tb 인자로 설정하고, __context__ 및 __cause__ 속성을 None으로 설정합니다.
새로운 API 함수 PyErr_SetContext(context)는 C 프로그래머가 연쇄 예외 정보를 제공하는 데 도움을 줍니다. 이 함수는 먼저 현재 예외를 인스턴스가 되도록 정규화한 다음, 해당 예외의 __context__ 속성을 설정합니다. 이와 유사한 API 함수 PyErr_SetCause(cause)는 __cause__ 속성을 설정합니다.
호환성
연쇄 예외는 가장 최근 예외의 유형을 노출하므로, 현재와 동일한 except 절과 여전히 일치합니다.
제안된 변경 사항은 예외 인스턴스에서 __context__, __cause__ 또는 __traceback__라는 이름의 속성을 설정하거나 사용하지 않는 한 어떤 코드도 중단하지 않습니다. 2005-05-12 현재, Python 표준 라이브러리에는 이러한 속성에 대한 언급이 없습니다.
미해결 문제: 추가 정보
Walter Dörwald [11]는 유형을 변경하지 않고 상향 전파 중인 예외에 추가 정보를 첨부하고 싶다는 의사를 표명했습니다. 이는 유용한 기능일 수 있지만, 이 PEP에서는 다루지 않습니다. 예외의 다른 정보 제공용 속성에 대한 규칙을 정하는 별도의 PEP를 통해 이를 다룰 수도 있습니다.
미해결 문제: 컨텍스트 억제
현재 작성된 방식에서는 __context__를 억제할 수 없습니다. except 또는 finally 절에서 exc.__context__를 None으로 설정해도 exc가 발생할 때 다시 설정될 뿐이기 때문입니다.
미해결 문제: 예외 유형 제한
캡슐화를 개선하기 위해 라이브러리 구현자는 모든 구현 수준 예외를 애플리케이션 수준 예외로 래핑하려 할 수 있습니다. 다음과 같이 작성하여 예외를 래핑할 수 있습니다.:
try:
... implementation may raise an exception ...
except:
import sys
raise ApplicationError from sys.exc_value
또는 다음과 같이 할 수 있습니다.:
try:
... implementation may raise an exception ...
except Exception, exc:
raise ApplicationError from exc
그러나 두 방법 모두 다소 결함이 있습니다. 포괄적인 except 절에서 현재 예외의 이름을 지정할 수 있으면 좋겠지만, 이는 여기서 다루지 않습니다. 이러한 기능이 있다면 다음과 같은 작업이 가능할 것입니다.:
try:
... implementation may raise an exception ...
except *, exc:
raise ApplicationError from exc
미해결 문제: yield
yield 문이 실행되면 예외 컨텍스트가 손실되며, yield 이후 프레임을 재개해도 컨텍스트가 복원되지 않습니다. 다음 예에서 보여 주듯이 이 문제는 새로운 문제가 아니지만, 이를 해결하는 것은 이 PEP의 범위를 벗어납니다.:
>>> def gen():
... try:
... 1/0
... except:
... yield 3
... raise
...
>>> g = gen()
>>> g.next()
3
>>> g.next()
TypeError: exceptions must be classes, instances, or strings
(deprecated), not NoneType
미해결 문제: 가비지 컬렉션
이 제안에 대한 가장 강력한 반론은 예외와 스택 프레임 사이에 순환이 생성된다는 점입니다 [12]. 순환 가비지의 수집이 크게 지연될 수 있으며, 따라서 리소스 해제도 크게 지연될 수 있습니다.
>>> try:
>>> 1/0
>>> except Exception, err:
>>> pass
이는 err -> traceback -> stack frame -> err의 순환을 도입하여, 다음 GC가 실행될 때까지 동일한 스코프의 모든 지역 변수를 살아 있게 합니다.
현재는 이러한 지역 변수들이 스코프를 벗어납니다. “지역” 리소스, 특히 열린 파일이 빠르게 닫힐 것이라고 가정하는 코드가 많습니다. 닫기가 다음 GC까지 기다려야 한다면, 현재는 정상적으로 실행되는 프로그램도 파일 핸들이 부족해질 수 있습니다.
__traceback__ 속성을 약한 참조로 만들면 순환 가비지로 인한 문제를 피할 수 있습니다. 안타깝게도 이렇게 하면 나중에 사용할 수 있도록 Exception을 저장하는 일이 (unittest가 하는 것처럼) 더 까다로워지고, sys 모듈을 충분히 정리할 수도 없게 됩니다.
Adam Olsen이 제안한 가능한 대안은 변수가 범위를 벗어날 때 스택 프레임에서 err 변수로 향하는 참조를 대신 약한 참조로 변환하는 것입니다 [13].
향후 호환 가능한 변경 사항
이러한 변경 사항은 인터프리터 수준에서 예외가 세 항목이 아니라 단일 객체로 나타나는 방식과 일치합니다.
- 관련 PEP 340 또는 PEP 343이 승인되면,
__exit__의 세 인자(type,value,traceback)를 단일 예외 인자로 대체하십시오. sys.exc_type,sys.exc_value,sys.exc_traceback및sys.exc_info()를 단일 멤버인sys.exception으로 대체하고 더 이상 사용하지 않도록 하십시오.sys.last_type,sys.last_value및sys.last_traceback을 단일 멤버인sys.last_exception으로 대체하고 더 이상 사용하지 않도록 하십시오.raise문장의 세 인자 형식을 한 인자 형식으로 대체하고 더 이상 사용하지 않도록 하십시오.cgitb.html()을 업그레이드하여 첫 번째 인자로(type, value, traceback)튜플 대신 단일 값을 허용하도록 하십시오.
향후 호환되지 않을 수 있는 변경 사항
이러한 변경 사항은 Python 3000에서 고려할 가치가 있을 수 있습니다.
sys.exc_type,sys.exc_value,sys.exc_traceback및sys.exc_info()를 제거하십시오.sys.last_type,sys.last_value및sys.last_traceback을 제거하십시오.- 세 인자를 사용하는
sys.excepthook을 한 인자를 사용하는 API로 교체하고, 이에 맞게cgitb모듈을 변경하십시오. raise문장의 세 인자 형식을 제거하십시오.traceback.print_exception을 업그레이드하여type,value및traceback인자 대신exception인자를 허용하도록 하십시오.
구현
__traceback__및 __cause__속성과 새로운 raise 구문은 리비전 57783에서 구현되었습니다 [14].
감사의 말
Brett Cannon, Greg Ewing, Guido van Rossum, Jeremy Hylton, Phillip J. Eby, Raymond Hettinger, Walter Dörwald 및 기타 인사
참고 문헌
Copyright
This document has been placed in the public domain.