for(i=0; i<NR; ++i) {
result = do_thing_1();
if (result < 0) {
if (result == IO_ERROR) {
/* handle error */
}
else if (result == API_ERROR) {
/* handle error */
}
else {
/* handle unknown error */
}
}
result = do_thing_2();
/* sigh ... I have to code another huge error block ... */
...
/* finally, the loop ends! */
}
result = do_thing_1();
if (result < 0) {
if (result == IO_ERROR) {
/* handle error */
}
else if (result == API_ERROR) {
/* handle error */
}
else {
/* handle unknown error */
}
}
result = do_thing_2();
/* sigh ... I have to code another huge error block ... */
...
/* finally, the loop ends! */
}
사실, C++에 예외 처리가 있지만, 표준 C-라이브러리 함수를 호출한다면 다시 세속적인 방식으로 돌아가야 한다.
위의 난잡한 코드를 파이썬으로 동등하게 다시 작성하면 다음과 같다:
try:
# uninterrupted program logic here, no need to break up
# the natural flow with error checking
for i in range(NR):
do_thing_1()
do_thing_2()
# errors can all be handled out-of-line
except IOERROR:
# handle IO error
except API_ERROR:
# handle API error
except:
# handle unknown error
# ** be careful here -- keep reading! **
# uninterrupted program logic here, no need to break up
# the natural flow with error checking
for i in range(NR):
do_thing_1()
do_thing_2()
# errors can all be handled out-of-line
except IOERROR:
# handle IO error
except API_ERROR:
# handle API error
except:
# handle unknown error
# ** be careful here -- keep reading! **
마지막 except 절 아래에 "be careful here"라고 써 놓은 부분에 주목하자. "be careful" 부분이 바로 이 글에서 다루고 싶은 부분이다. 다루고자 하는 주제는 다음과 같다:
- 처리되지 못한 에러를 전역적으로 잡기.
- 예외 잡기, 자세하게 설명.
- 여러 예외를 잡기.
- "맨" 예외를 사용하면 좋을 때와 나쁠 때.
처리되지 못한 에러를 전역적으로 잡기.
(나의 경험에 의하면) 파이썬에는 최고의 디버깅 도구중의 하나로 특별한 갈고리인 sys.excepthook가 있다. 본인은 언제나 이를 자동으로 처리하는 맞춤 모듈을 나의 프로젝트에 포함시킨다. 작업의 대부분은 (표준 라이브러리에서) cgitb라는 한 모듈이 담당한다. "cgitb"라고 불리우는 이유는 그의 원래 의도가 CGI 스크립트에서 일어난 에러 역추적을 보여주기 위한 것이었기 때문이다. 그러나 다른 종류의 프로그램에서도 똑 같이 쉽게 사용할 수 있다. 다음은 나의 맞춤 에러 처리 모듈이다. 잘라 붙여 넣어보자:Module errors.py
import sys, cgitb
from datetime import datetime
def catch_errors():
sys.excepthook = my_except_hook
def my_except_hook(etype, evalue, etraceback):
do_verbose_exception( (etype,evalue,etraceback) )
def do_verbose_exception(exc_info=None):
if exc_info is None:
exc_info = sys.exc_info()
txt = cgitb.text(exc_info)
d = datetime.now()
p = (d.year, d.month, d.day, d.hour, d.minute, d.second)
filename = "ErrorDump-%d%02d%02d-%02d%02d%02d.txt" % p
open(filename,'w').write(txt)
print "** EXITING on unhandled exception - See %s" % filename
sys.exit(1)
from datetime import datetime
def catch_errors():
sys.excepthook = my_except_hook
def my_except_hook(etype, evalue, etraceback):
do_verbose_exception( (etype,evalue,etraceback) )
def do_verbose_exception(exc_info=None):
if exc_info is None:
exc_info = sys.exc_info()
txt = cgitb.text(exc_info)
d = datetime.now()
p = (d.year, d.month, d.day, d.hour, d.minute, d.second)
filename = "ErrorDump-%d%02d%02d-%02d%02d%02d.txt" % p
open(filename,'w').write(txt)
print "** EXITING on unhandled exception - See %s" % filename
sys.exit(1)
에러 처리자가 잡을 "작성실수(thinko)" 버그.
# enable catching of unhandled exceptions
import errors
errors.catch_errors()
def do_thing_1(value):
# just coding along, not expecting anything to go wrong ...
for i in range(20):
print "ratio is %d" % (value/(10-i))
do_thing_1(100)
import errors
errors.catch_errors()
def do_thing_1(value):
# just coding along, not expecting anything to go wrong ...
for i in range(20):
print "ratio is %d" % (value/(10-i))
do_thing_1(100)
ratio is 10
ratio is 11
ratio is 12
ratio is 14
ratio is 16
ratio is 20
ratio is 25
ratio is 33
ratio is 50
ratio is 100
** EXITING on unhandled exception - See ErrorDump-20060305-110304.txt
덤프 파일을 보면 무슨 일이 일어났는지 상세하게 알 수 있다. 에러가 일어날 때 지역 변수의 값뿐만 아니라 함수 매개변수들을 어떻게 보여주는지 주목하자 (do_thing_1(value=100)). 보통은 테스트 사례를 다시 실행시키고 회돌이를 돌면서 이 값들이 무엇인지 보아야 한다. 이제 충돌이 일어나자 마자 프로그램 상태의 찰칵그림을 얻는다.
ratio is 11
ratio is 12
ratio is 14
ratio is 16
ratio is 20
ratio is 25
ratio is 33
ratio is 50
ratio is 100
** EXITING on unhandled exception - See ErrorDump-20060305-110304.txt
"ErrorDump-20060305-110304.txt"의 내용
ZeroDivisionError
Python 2.4.2: /usr/bin/python
Sun Mar 5 11:03:04 2006
A problem occurred in a Python script. Here is the sequence of
function calls leading up to the error, in the order they occurred.
/var/www/localhost/htdocs/python/tourist/t.py
9 print "ratio is %d" % (value/(10-i))
10
11 do_thing_1(100)
12
do_thing_1 = <function do_thing_1>
/var/www/localhost/htdocs/python/tourist/t.py in do_thing_1(value=100)
7 # just coding along, not expecting anything to go wrong ...
8 for i in range(20):
9 print "ratio is %d" % (value/(10-i))
10
11 do_thing_1(100)
value = 100
i = 10
ZeroDivisionError: integer division or modulo by zero
__doc__ = 'Second argument to a division or modulo operation was zero.'
__getitem__ = <bound method ZeroDivisionError.__getitem__ of <exceptions.ZeroDivisionError instance>>
__init__ = <bound method ZeroDivisionError.__init__ of <exceptions.ZeroDivisionError instance>>
__module__ = 'exceptions'
__str__ = <bound method ZeroDivisionError.__str__ of <exceptions.ZeroDivisionError instance>>
args = ('integer division or modulo by zero',)
The above is a description of an error in a Python program. Here is
the original traceback:
Traceback (most recent call last):
File "t.py", line 11, in ?
do_thing_1(100)
File "t.py", line 9, in do_thing_1
print "ratio is %d" % (value/(10-i))
ZeroDivisionError: integer division or modulo by zero
Python 2.4.2: /usr/bin/python
Sun Mar 5 11:03:04 2006
A problem occurred in a Python script. Here is the sequence of
function calls leading up to the error, in the order they occurred.
/var/www/localhost/htdocs/python/tourist/t.py
9 print "ratio is %d" % (value/(10-i))
10
11 do_thing_1(100)
12
do_thing_1 = <function do_thing_1>
/var/www/localhost/htdocs/python/tourist/t.py in do_thing_1(value=100)
7 # just coding along, not expecting anything to go wrong ...
8 for i in range(20):
9 print "ratio is %d" % (value/(10-i))
10
11 do_thing_1(100)
value = 100
i = 10
ZeroDivisionError: integer division or modulo by zero
__doc__ = 'Second argument to a division or modulo operation was zero.'
__getitem__ = <bound method ZeroDivisionError.__getitem__ of <exceptions.ZeroDivisionError instance>>
__init__ = <bound method ZeroDivisionError.__init__ of <exceptions.ZeroDivisionError instance>>
__module__ = 'exceptions'
__str__ = <bound method ZeroDivisionError.__str__ of <exceptions.ZeroDivisionError instance>>
args = ('integer division or modulo by zero',)
The above is a description of an error in a Python program. Here is
the original traceback:
Traceback (most recent call last):
File "t.py", line 11, in ?
do_thing_1(100)
File "t.py", line 9, in do_thing_1
print "ratio is %d" % (value/(10-i))
ZeroDivisionError: integer division or modulo by zero
초보 파이썬 프로그래머로서, 나는 처음에 무엇에나 try ... except 절을 두르고 싶은 생각이 들었다. 잠시 지나자, 오직 깨끗이 청소할 필요가 있거나 에러 다음에 롤백할 필요가 있는 상태 정보가 있는 곳에만 try .. except절을 두는 것이 훨씬 더 좋다는 것을 깨닫았다. ("작성실수(thinko)" 사례와 같은) 다른 에러들은 전역 처리자에 이르도록 그대로 두는 편이 더 좋다. except 절에 너무 욕심을 내면 실제로 정보를 잃어버릴 수 있다.
멋지게 다음 주제로 나가 보자 ...
예외 잡기, 자세하게.
예외를 아래와 보이는 바와 같이 잡으면, 정보의 일부만 얻을 것이다:try:
... do some stuff ...
except API_ERROR:
#
# OK, I know an API_ERROR occurred, but have no other details!
#
다음은 except 절에 좀 더 많은 정보를 제공하고 사용하는 법을 보여주는 작은 예제이다.
... do some stuff ...
except API_ERROR:
#
# OK, I know an API_ERROR occurred, but have no other details!
#
class ParseError(Exception):
"""
Custom exception class to capture details on a
parsing error:
txt = Will be shown by default exception handler.
filename,line,col = Where the error occurred.
"""
def __init__(self, txt, filename, line, col):
Exception.__init__(self, txt)
self.filename = filename
self.line = line
self.col = col
def parsefile(filename):
for line in open(filename,'r'):
# ... parsing code ...
# Say I find an error in line 20, column 10 ...
line = 20
col = 10
raise ParseError('Parse Error, file=%s line=%d,col=%d' % \
(filename,line,col),
filename, 20, 10)
# If I do nothing, Python will show the 'txt' as the error.
parsefile('t.py')
"""
Custom exception class to capture details on a
parsing error:
txt = Will be shown by default exception handler.
filename,line,col = Where the error occurred.
"""
def __init__(self, txt, filename, line, col):
Exception.__init__(self, txt)
self.filename = filename
self.line = line
self.col = col
def parsefile(filename):
for line in open(filename,'r'):
# ... parsing code ...
# Say I find an error in line 20, column 10 ...
line = 20
col = 10
raise ParseError('Parse Error, file=%s line=%d,col=%d' % \
(filename,line,col),
filename, 20, 10)
# If I do nothing, Python will show the 'txt' as the error.
parsefile('t.py')
출력결과
Traceback (most recent call last):
File "t.py", line 27, in ?
parsefile('t.py')
File "t.py", line 24, in parsefile
filename, 20, 10)
__main__.ParseError: Parse Error, file=t.py line=20,col=10
File "t.py", line 27, in ?
parsefile('t.py')
File "t.py", line 24, in parsefile
filename, 20, 10)
__main__.ParseError: Parse Error, file=t.py line=20,col=10
# Catch it so I can access .filename, .line, and .col
try:
parsefile('t.py')
except ParseError, info:
# Now I can do whatever I want to with the detailed info
print "CAUGHT! Parse error in %s at line=%d, column=%d" % \
(info.filename, info.line, info.col)
try:
parsefile('t.py')
except ParseError, info:
# Now I can do whatever I want to with the detailed info
print "CAUGHT! Parse error in %s at line=%d, column=%d" % \
(info.filename, info.line, info.col)
출력결과
CAUGHT! Parse error in t.py at line=20, column=10
경고
except 절은 정확하게 except ParseError, info와 같이 되어야 한다.
except (ParseError,info)이나 except [ParseError,info]를 사용하려고 시도해 보면, 작동하지 않을 것이다.
편리하게도, 그 때문에 다음 주제로 나아가게 된다 ...
여러 예외를 잡기.
단 하나의 except 절로 여러 예외를 잡을 수 있으면 편리한 경우가 많다. 아래의 예에서, 함수에 건네지는 나쁜 유형을 점검하겠다. 그리고 에러가 탐지되면, 유형마다 따로 예외를 일으키겠다."""
As in the previous example, I will place useful info into
the 'txt' parameter to the base Exception class. This way
the caller can see exactly what happened without having
to catch the exception and look at the 'info' parameter.
"""
class ErrNeedList(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a list for '%s'" % parm)
self.parm = parm
self.usage = "Need a list"
class ErrNeedDict(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a dictionary for '%s'" % parm)
self.parm = parm
self.usage = "Need a dictionary"
class ErrNeedString(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a string for '%s'" % parm)
self.parm = parm
self.usage = "Need a string"
def test_function(a_list, a_dict, a_string):
# check for type errors
if not isinstance(a_list, list):
raise ErrNeedList('a_list')
if not isinstance(a_dict, dict):
raise ErrNeedDict('a_dict')
if not isinstance(a_string, str):
raise ErrNeedString('a_string')
# cause errors and catch them
try:
test_function( 1,2,3)
#------------------------------------------------------
# here I can test for all errors at once - since each
# has a .parm and .usage attribute, I can treat them
# the same way
#------------------------------------------------------
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
try:
test_function( [],2,3)
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
try:
test_function( [],{},3)
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
As in the previous example, I will place useful info into
the 'txt' parameter to the base Exception class. This way
the caller can see exactly what happened without having
to catch the exception and look at the 'info' parameter.
"""
class ErrNeedList(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a list for '%s'" % parm)
self.parm = parm
self.usage = "Need a list"
class ErrNeedDict(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a dictionary for '%s'" % parm)
self.parm = parm
self.usage = "Need a dictionary"
class ErrNeedString(Exception):
def __init__(self, parm):
Exception.__init__(self, "Need a string for '%s'" % parm)
self.parm = parm
self.usage = "Need a string"
def test_function(a_list, a_dict, a_string):
# check for type errors
if not isinstance(a_list, list):
raise ErrNeedList('a_list')
if not isinstance(a_dict, dict):
raise ErrNeedDict('a_dict')
if not isinstance(a_string, str):
raise ErrNeedString('a_string')
# cause errors and catch them
try:
test_function( 1,2,3)
#------------------------------------------------------
# here I can test for all errors at once - since each
# has a .parm and .usage attribute, I can treat them
# the same way
#------------------------------------------------------
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
try:
test_function( [],2,3)
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
try:
test_function( [],{},3)
except (ErrNeedList, ErrNeedDict, ErrNeedString), info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
Output
CAUGHT API ERROR in parameter: a_list - Need a list
CAUGHT API ERROR in parameter: a_dict - Need a dictionary
CAUGHT API ERROR in parameter: a_string - Need a string
CAUGHT API ERROR in parameter: a_dict - Need a dictionary
CAUGHT API ERROR in parameter: a_string - Need a string
모든 예외가 공통 속성을 가지는, 이와 같은 예제에서는 단 하나의 바탕 클래스로부터 모든 예외를 파생시키는 것이 합리적이다. 클래스가 공통 클래스 APIError로부터 파생되도록 재작성하면:
"Base class"
class APIError(Exception):
def __init__(self, txt, parm, usage):
Exception.__init__(self, txt)
self.parm = parm
self.usage = usage
class ErrNeedList(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a list for '%s'" % parm,
parm, "Need a list")
class ErrNeedDict(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a dictionary for '%s'" % parm,
parm, "Need a dictionary")
class ErrNeedString(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a string for '%s'" % parm,
parm, "Need a string")
이제 예외를 좀 더 간결하게 잡을 수 있다:
class APIError(Exception):
def __init__(self, txt, parm, usage):
Exception.__init__(self, txt)
self.parm = parm
self.usage = usage
class ErrNeedList(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a list for '%s'" % parm,
parm, "Need a list")
class ErrNeedDict(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a dictionary for '%s'" % parm,
parm, "Need a dictionary")
class ErrNeedString(APIError):
def __init__(self, parm):
APIError.__init__(self, "Need a string for '%s'" % parm,
parm, "Need a string")
try:
test_function( [],{},3)
#
# Now I can just catch the baseclass, and it will catch
# all subclasses as well!
#
except APIError, info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
test_function( [],{},3)
#
# Now I can just catch the baseclass, and it will catch
# all subclasses as well!
#
except APIError, info:
print "CAUGHT API ERROR in parameter: %s - %s" % (info.parm, info.usage)
경고
여러 예외를 잡을 때는 터플을 사용해야 한다. 즉 except (Err1,Err2,Err3)와 같이 말이다. 리스트를 사용하려고 하면, 즉 except [Err1,Err2,Err3]와 같이, 예외를 잡지 않을 것이고, 더 나쁘게는 파이썬이 그것을 구문 에러로 표식할 것이다.
"맨" 예외를 사용하면 좋을 때와 나쁠 때.
본인이 처음 예외에 관하여 배울 때, 코드 블록을 다음과 같이 작성하는 것이 좋은 생각 같아 보였다:try:
... do something ...
except:
print "Got an error!"
... do something ...
except:
print "Got an error!"
블록 안에서 모든 에러를 확실하게 잡을 수 있기 때문에 나에게는 처음에는 튼튼해 보였다. 여기에는 세 가지 문제가 있다:
- 예외 유형이 지정되어 있지 않다. 그래서 어떤 종류의 에러가 일어났는지 알지 못한다.
- info 매개변수가 주어지지 않았다. 그래서 주어진 기타 추가 정보를 모두 버리고 있다.
- 일어나리라 예상한 에러를 잡을 뿐만 아니라, 본질적으로 일어나리라 예상하지 못한 에러도 가려 버린다:
try:
# I could get an IOError here if "filein.txt" doesn't exist,
# or is not readable.
fin = open('t.py','r')
# I could get an IOError here if I do not have write-permissions
# in the current directory, or the disk is out of space.
fout = open('fileout.txt','w')
# I don't expect anything bad to happen here ...
for line in fin:
# filter out comment lines
if re.match('^\s*#$', line):
continue
except:
# The only errors I *expect* are IOErrors, so obviously
# I can say this ... or can I??
print "A file error occurred."
이제 이 예제를 실행하고 다음과 같은 결과를 얻었다:A file error occurred.
그래서 디버깅을 시작했다. 오직 IOError만 일어난다고 예상하고서, 일어날 수 있는 일들을 목록으로 만들었다:- t.py는 존재하지 않는다
- t.py는 읽을 수 없다
- 권한이 부족하기 때문에 또는 디스크가 용량이 모자라기 때문에 fileout.txt를 만들 수 없다.
except 절이 실제 문제를 보여주도록 재작성하면:try:
# I could get an IOError if "filein.txt" doesn't exist,
# or is not readable.
fin = open('t.py','r')
# Same here, if I cannot create "fileout.txt"
fout = open('fileout.txt','w')
for line in fin:
# filter out comment lines
if re.match('^\s*#$', line):
continue
# Only catch what I am PREPARED to handle!
except IOError:
print "A file error occurred."
아! 물론, 사용하기 전에 re를 반입하는 것을 깜빡하고 잊어버렸다.출력결과Traceback (most recent call last):
File "t.py", line 11, in ?
if re.match('^s*$', line):
NameError: name 're' is not defined
이것으로 왜 처리하도록 지정한 구체적 에러만 잡고, 다른 것들은 최상위 수준까지 흘러가도록 놓아두어야 하는지 그 이유를 보여주었기를 바란다 (거기에서 에러를 잡을 수 있다. 즉, 앞서 기술한 "global hook"으로 말이다).
가끔, 자격없는 "excepts"도 좋다!
이제, 즉시 내 말에 모순되게 어떤 경우는 자격없는 except 절이 좋으며, 심지어 바람직한 경우도 있음을 언급하겠다. 내가 항상 마주하는 한 가지 예는 GUI 코드이다. 마우스 커서에 "busy" 상태를 설정하고 나서 기나긴 연산을 시작한다. 중간에 충돌이 일어나면, 영원히 커서에 바쁜 상태를 그대로 두고 싶지는 않을 것이다. 그렇게 되면 사용자가 혼란스러울 수 있다. 다음은 그런 상황에 내가 코드하는 전형적인 방식이다 (여기에서는 wxPython을 사용한다):#
# I'm about to perform a long operation, so set the
# cursor to the "busy" (hourglass) state
#
wx.BeginBusyCursor()
try:
# A long operation begins here ...
...
...
# when I'm finished, exit the busy state
wx.EndBusyCursor()
except:
# cancel the busy state - don't leave the user hanging!
wx.EndBusyCursor()
# re-raise original error to caller
raise마지막 줄이 중요하다: 맨 raise 서술문을 사용하면, 원래 예외를 다시 일으킬 것이다. 그래서 그 에러는 다시 호출자에게 정보의 손실없이 전파된다.
데이터베이스 트랜잭션에 관련된 상황에서 자격없는 except 절도 사용했다:경고다음과 같이 하고 싶지 않을 것이다:try:
... stuff ...
except Exception, exc:
raise exc
원래 예외로부터 정보를 잃어버리기 때문이다.# "pseudo-SQL" code, just to give the idea ...
try:
# run entire transaction inside of "try"
sql.run("begin transaction")
sql.run("insert into ...")
sql.run("insert into ...")
sql.run("update ...")
sql.run("commit transaction")
except:
# undo any changes on error
sql.run("rollback transaction")
# propagate original error
raise
이는 아주 멋지다. 호출자는 에러가 일어났는지 안다. 그러나, 이미 깨끗이 정리되었기 때문에 데이터베이스 상태에 관하여 신경쓸 필요가 없다.
자격없는 절이 유용한 마지막 예는 쓰레드 프로그램에 있는데 여기에서 나는 전역 데이터 집합을 잠근다:from threading import Lock
DATA_LOCK = Lock()
try:
DATA_LOCK.acquire()
.. perform operations on global data ...
DATA_LOCK.release()
except:
# unlock on error
DATA_LOCK.release()
# propagate original error
raiseWritten in WikklyText.