PEP 344 – 예외 연쇄와 내장 트레이스백
- Author:
- Ka-Ping Yee
- Status:
- Superseded
- Type:
- Standards Track
- Created:
- 12-May-2005
- Python-Version:
- 2.5
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
번호에 관한 참고
이 PEP의 번호는 PEP 3134로 변경되었습니다. 아래 내용은 이전 번호로 제출된 마지막 버전입니다.
초록
이 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, xc_chain, excprev, previous, precursor 등 여러 이름이 제안되었습니다. 명시적으로 연쇄된 예외에 대해서는 그 구체적인 의미 때문에 이 PEP는 __cause__를 제안합니다. 암시적으로 연쇄된 예외에 대해서는 이 PEP가 __context__라는 이름을 제안합니다. 의도한 의미가 시간적 선후 관계보다는 구체적이지만 인과 관계보다는 덜 구체적이기 때문입니다. 즉, 어떤 예외가 다른 예외를 처리하는 맥락에서 발생한다는 뜻입니다.
이 세 속성은 Python VM이 설정하므로, 이 PEP는 이 속성들에 앞뒤로 이중 밑줄이 붙은 이름을 제안합니다. 아주 특별한 경우에만 이 속성들을 일반 대입으로 설정해야 합니다.
이 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__속성이 아직 없으면, 인터프리터는 이 속성을 스레드의 예외 컨텍스트로 설정합니다. - 예외가 발생한 직후, 스레드의 예외 컨텍스트는 그 예외로 설정됩니다.
- 인터프리터가
except블록의 끝에 도달하거나return,yield,continue,break문을 실행하여 그 블록을 빠져나올 때마다, 스레드의 예외 컨텍스트는None으로 설정됩니다.
명시적 예외 연쇄
예외 객체의 __cause__ 속성은 항상 None으로 초기화됩니다. 이 속성은 새로운 형태의 raise 문으로 설정됩니다:
raise EXCEPTION from CAUSE
이는 다음과 동등합니다:
exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc
다음 예에서 데이터베이스는 몇 가지 서로 다른 종류의 저장소에 대한 구현을 제공하며, 파일 저장소가 그중 하나입니다. 데이터베이스 설계자는 클라이언트가 저장소별 세부 사항을 알 필요가 없도록 오류가 DatabaseError 객체로 전파되기를 원하지만, 근본적인 오류 정보를 잃고 싶지는 않습니다:
class DatabaseError(StandardError):
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 functions는 기본값이 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에서 다룰 수도 있을 것입니다.
미해결 문제: 컨텍스트 억제
현재 작성된 대로라면 이 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 -> 스택 프레임 -> err로 이어지는 순환을 만들어, 다음 GC가 일어날 때까지 같은 스코프에 있는 모든 지역 변수를 살아 있게 합니다.
현재는 이 지역 변수들이 스코프를 벗어나게 됩니다. “지역” 자원, 특히 열린 파일이 빠르게 닫힐 것이라고 가정하는 코드가 많이 있습니다. 닫는 작업이 다음 GC까지 기다려야 한다면, (현재는 잘 실행되는) 프로그램이 파일 핸들을 모두 소진할 수 있습니다.
__traceback__ 속성을 약한 참조로 만들면 순환 가비지 문제를 피할 수 있습니다. 안타깝게도 그렇게 하면 (unittest가 하는 것처럼) Exception을 나중을 위해 저장하는 일이 더 번거로워지며, 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인자를 받도록 개선합니다.
감사의 글
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.