7장. 예외 처리와 디버깅

모든 종류의 좋은 프로그램은 언어에서 제공하는 예외처리 메커니즘을 사용해서 만들어진다. 여러분이 만든 소프트웨어로 일을 처리하는 중에 프로그램이 죽어서 화면에 보기싫은 에러 메시지를 띄우는 것보다 최종 사용자를 절망에 빠트리는데 더 좋은 방법은 없다. 예외 처리는 여러분의 프로그램이 문제를 만났을 때 수행을 계속하면서 최종 사용자 또는 프로그램 관리자에게 그에 대한 정보를 제공할 수 있도록 보장하는 일에 관련된 모든 것을 망라한다. 자바 프로그래머들은 예외를 다루는 일에 익숙해지기 마련인데, 일부 자바코드는 try-catch-finally 문 안에서 예외를 처리하지 않으면 컴파일조차 되지 않는다. 파이썬은 자바와 유사한 구조를 가지고 있으며, 우리는 이 장에서 그들을 다룰것이다.

예외를 찾았을 때에는, 혹은 소프트웨어를 배포하기 전에는, 여러분은 잘못된 코드를 찾아서 고치기 위해 코드를 뒤져서 디버그해야 한다. 코드를 디버깅하고 고치는 방법에는 여러 가지가 있는데, 이번 장에서 몇 가지 방법을 다루어 볼 것이다. 자바에서와 마찬가지로 파이썬에서도, assert 키워가 이러한 부분에서 상당한 도움이 될 것이다. 여기서는 단정에 대하여 깊이 있게 다룰 것이며, 찾기 어려운 에러를 디버깅하는데 걸리는 시간을 줄여주고 여러분을 도와줄 다른 방법들을 배울 것이다.

예외 처리 구문

자바 개발자들은 예외처리를 하는데 사용되는 주요 기제인 try-catch-finally 구문과 매우 친숙하다. 파이썬의 예외 처리는 자바에서 차이가 있기는 하지만, 구문은 거의 비슷하다. 어쨌든, 자바는 예외를 코드로 던지는 방법이 조금 다르다. 자, 던진다(throw)라고 하는 것은 자바에서 쓰이는 표현이다. 파이썬에서는 예외를 던지는 것이 아니라, 그저 예외를 일으킨다(raise). 서로 다르게 표현하고는 있지만 기본적으로 두 용어는 같은 것이다. 이번 장에서, 우리는 파이썬 코드에서 예외들을 발생시키고 처리하는 프로세스를 다룰 것이다. 그리고 자바에서 사용하는 것과는 어떻게 다른지 살펴볼 것이다.

예외에 익숙하지 않은 사람을 위해, 자바 언어에서 예외를 다루는 법을 살펴보도록 하겠다. 여러분은 이를 통하여 두 언어의 구문을 비교할 수 있으며, 파이썬이 제공하는 유연성에 고마움을 느끼게 될 것이다.

예제 7-1. 자바에서의 예외 처리

try {
// 예외를 던질 수있는 몇 가지 작업을 수행
} catch (ExceptionType messageVariable) {
// 몇 가지 예외 처리를 수행
} finally {
// 항상 호출해야하는 코드를 실행
}

이제는 파이썬에서 이 작업을 하는 방법에 대해 자세히 알아보자. 예외들을 발생시키고, 다루는 방법을 배울 뿐만아니라, 단정문을 사용하는 것과 같은 훌륭한 기법 또한 뒤에서 배울 것이다.

예외 잡기

여러분은 프로그램 수행 중에 작동이 정지되면서 지저분한 메시지가 표시되는 일을 어느 정도로 자주 겪는가? 대부분의 예외는 잡아서 잘 처리할 수 있으므로 그러한 일이 일어나는 것을 줄일 수가 있다. 예외를 잘 처리한다는 것은, 프로그램을 정지하지 않으면서 최종 사용자에게 무엇이 문제인지를, 때에 따라서는 문제를 어떻게 해결할 수 있는지를 설명하는 오류 메시지를 전달한다는 의미이다. 프로그래밍 언어 내에서 예외를 처리하는 기제는 이러한 목적을 위해 개발되었다.

예제 7-2. try-except 예제

# 다음 함수는 try-except 문을 통하여 사용자가 0을 분모로 전달하면
# 친절한 에러 메세지를 제공한다.
>>> from __future__ import division
>>> def divide_numbers(x, y):
...     try:
...         return x/y
...     except ZeroDivisionError:
...         return 'You cannot divide by zero, try again'
…
# 8 나누기 3을 시도
>>> divide_numbers(8,3)
2.6666666666666665
# 8 나누기 0을 시도
>>> divide_numbers(8, 0)
'You cannot divide by zero, try again'

각각의 설명과 함께 파이썬 언어에 내장되어 있는 모든 예외가 표 7-1에 나열되어 있다. 이들 중 어느 것이든 사용하여 예외를 일으키고, 그에 대한 처리를 시도해 볼 수 있다. 예외를 일으키고자 할 때에 어떻게 하는지 뒤에서 살펴보도록 하겠다. 마지막으로, 이 중에 있는 예외와 맞지 않는 특정 예외 형식이있다면, 자신의 예외 유형 개체를 작성할 수 있다. 파이썬 예외 처리는 자바에서의 예외 처리와 약간의 차이점이 있음에 유의하기 바란다. 자바에서, 컴파일러는 예외를 잡는 것을 여러번 강제적으로 시키는데, 이러한 것을 검사 예외(checked exception)라고 한다. 검사 예외는 기본적으로 몇 가지 작업을 수행하는 동안 메소드가 던질 수 있는 예외이다. 개발자는 이러한 검사 예외를 try/catch 또는 throws 문을 이용하여 반드시 처리해야 하며, 그렇지 않으면 컴파일러의 불평을 듣게 된다. 파이썬의 오류 처리 장치에서는 그런 식으로 예외 처리를 강제하지는 않으며, 예외를 다룰 것인지 말 것인지에 대한 결정은 개발자의 몫이다. 인터프리터가 강제하지 않는더라도 가능하면 오류 처리를 포함하는 것이 좋다.

파이썬에서 Exception은 언어에 내장된 특수 클래스이다. 따라서, Exception은 예외에 대한 클래스 계층 구조이며, 몇 가지 예외들은 실제로 다른 예외 클래스의 하위 클래스이다. 이 경우, 프로그램은 그런 예외의 수퍼 클래스를 처리할 수 있으며, 모든 subclassed 예외들은 자동으로 처리된다. 표 7-1은 파이썬 언어에 정의된 예외목록이며, 들여쓰기는 클래스 계층 구조와 비슷하다.

표 7-1. 예외

예외 설명
BaseException 모든 다른 예외들의 근원(root)이 되는 예외
GeneratorExit
반복의 종료를 위한 것으로서 generators의 close() 메소드에
의하여 일어남
KeyboardInterrupt 끼어들기 키에 의하여 일어남
SystemExit 프로그램 종료
Exception 모든 non-exiting 예외의 근원
StopIteration 반복(iteration)의 종료를 위하여 발생
StandardError 모든 내장 예외의 기본 클래스
ArithmeticError 모든 수치연산 관련 예외의 기본이 됨
FloatingPointError 부동소수 연산이 실패할 때 발생
OverflowError 너무 큰 수치 연산
ZeroDivisionError 나눗셈 또는 나머지 연산 수행시 0으로 나눌 경우
AssertionError 단정문이 실패할 때 발생
AttributeError 속성의 참조 또는 할당 실패
EnvironmentError 파이썬의 외부적인 요인에 의한 오류
IOError 입출력 연산 오류
OSError os 모듈에서 발생하는 오류
EOFError 파일의 끝을 지나쳐서 input() 또는 raw_input()을 시도
ImportError 들여올(import) 모듈이나 이름을 찾지 못함
LookupError IndexError와 KeyError의 기본 클래스
IndexError 순서 색인이 범위를 벗어남
KeyError 존재하지 않는 사상(사전)의 키를 참조
MemoryError 기억장소 소진
NameError 지역 또는 전역의 이름을 찾는 데 실패
UnboundLocalError 할당되지 않은 지역 변수를 참조
ReferenceError 쓰레기 수거된 개체에 대한 접근을 시도
RuntimeError 모든 경우에 대한 쓸모 없는 오류
NotImplementedError 기능이 구현되지 않은 경우에 발생
SyntaxError 구문분석기가 구문 오류를 만남
IndentationError 구문분석기가 들여쓰기 문제를 만남
TabError 탭과 스페이스를 잘못 섞어 씀
SystemError 치명적이지 않은 번역기 오류
TypeError 연산자 또는 함수에 부적당한 형이 전달됨
ValueError TypeError 또는 다른 명확한 오류에 해당하지 않는 인자 오류
Warning 모든 경고의 기본

try-except-finally 블록은 파이썬 프로그램에서 예외 처리 작업을 수행하기 위하여 사용된다. 자바와 상당히 비슷하게, 예외를 일으키는 코드나 그렇지 않은 코드를 try 블록에 넣을 수 있다. 예외를 잡는 부분은 except 블록으로 자바에서 catch에 해당한다. 예외가 던져지든 아니든 상관없이 수행되어야하는 작업은 finally 블록에 들어가면 된다. except 블록 내에서 예외가 일어나거나 혹은 다른 예외가 일어나더라도 finally 블록 내의 모든 작업은 수행된다. 프로그램이 완료되는 것을 보장하기 위해 예외가 일어나기 전의 작업도 수행된다. finally 블록은 파일을 닫는 것과 같은 정리 작업을 수행하기에 훌륭한 장소이다.

예제 7-3. try-except-finally 논리

try:
    # 예외를 유발할 수 있는 작업을 수행
except Exception, value:
    # 예외 처리를 수행
finally:
    # 항상 완료되어야하는 작업을 수행(예외를 발생시키기 전에 처리됨)

파이썬에서는 또한 else 절을 제공하여 try-except-else 논리를 구성할 수 있다. else는 선택사항으로서 아무런 예외도 찾을 수 없을 경우에 수행할 코드를 넣을 수 있다.

예제 7-4. try-finally 논리

try:
    # 예외를 유발할 수 있는 작업을 수행
finally:
    # 항상 완료되어야하는 작업을 수행(예외를 발생시키기 전에 처리됨)

아무런 예외가 발생하지 않을 경우에만 수행하여야 하는 작업과 같은 예외 처리 논리를 다루기 위하여 else 절을 사용할 수 있다. else 절 내의 코드는 아무런 예외도 던져지지 않았을 때에만 시작되며, else 절 내에서 어떠한 예외가 발생하게 되면 except로 돌아가지 못한다. else 절에 위치할 활동으로는 데이터베이스의 확약commit과 같은 거래가 있다. 여러 건의 데이터베이스 거래가 try 절을 차지하고 있다면, 아무런 예외도 발생하지 않아야만 확약을 하고자 할 것이다.

예제 7-5. try-except-else 논리

try:
    # 예외를 발생시킬 수 있는 작업을 수행
except:
    # 예외 처리를 수행
else:
    # 아무 예외도 던져지지 않았을 때에만 할 일을 수행

except 블록 내에서 검출할 예외의 종류를 정할 수도 있고, 예외의 이름을 정하지 않음으로써 일반적인 예외처리를 정의할 수도 있다. 물론 발생할 법한 예외의 유형에 따라 적절하게 처리해주는 것이 가장 좋다. 결국에는, 만약에 프로그램이 지저분한 에러를 내뱉는다면, 예외 처리 블록은 사용자 친화적이지 못하고 개발자에게만 쓸모가 있을 것이다. 그렇지만, 오류를 무시하고 계속 진행하고자 할 경우에는 예외 유형을 명시적으로 참조하지 않는 것이 나은 경우도 있기는 하다. except 블록에는 예외 메시지를 할당하기 위한 변수를 정의할 수 있다. 이를 통하여 메시지를 저장하였다가 예외 처리 코드 블록에서 표출할 수 있다. 자이썬에서 자바 코드를 호출하는데 자바 코드가 예외를 던진다면, 자이썬에서는 이를 자이썬의 예외와 동일한 방법으로 처리할 수 있다.

예제 7-6. 파이썬에서의 예외 처리

# 예외 처리가 없는 코드
>>> x = 10
>>> z = x / y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined
# 같은 일을 하되 예외 처리 블록이 있는 코드
>>> x = 10
>>> try:
...     z = x / y
... except NameError, err:
...     print "One of the variables was undefined: ", err
...
One of the variables was undefined:  name 'y' is not defined

자이썬 2.5.x에서는 파이썬 2.5.x의 예외 처리 구문을 사용함에 유의하기 바란다. 앞으로 나올 자이썬 릴리스에서는 구문이 변경될 수 있다. 예외를 hold하는 변수를 정의하는 데 쓰이는 구문을 눈여겨보도록 하자. 다시말해서, 예외유형(ExceptionType)을 제외하고, 파이썬 및 자이썬 2.5와 그 이후 버전에서 값 서술 구문은 다르다. 파이썬 3에서는 새로운 구문만 사용할 수 있기 때문에 개발자들이 그에 대비할 수 있도록 하기 위하여 파이썬 2.6에서 구문이 변경되었다.

예제 7-7. 자이썬 및 파이썬 2.5 이전

try:
    # 코드
except ExceptionType, messageVar:
    # 코드

예제 7-8. 자이썬 2.6 (미구현) 및 파이썬 2.6 이후

try:
   # 코드
except ExceptionType as messageVar:
    # 코드

예외 처리 코드를 작성함에 있어서 예외의 형을 명시하지 않는 것은 좋지 못한 프로그래밍 방법이라는 점은 앞서 언급하였다. 그렇기는 하지만, 파이썬에는 던져진 예외의 형을 알아낼 수 있는 두 가지 수단을 제공한다. 예외 유형을 찾는 쉬운 방법은 앞서 논의한 것과 같이 예외를 잡아서 변수로 취급하는 것이다. 그런 다음 필요에 따라 type(오류변수) 구문을 사용하여 특정한 예외 유형을 찾아낼 수 있다.

예제 7-9. 예외 유형 판별

# 이 예제에서, 우리는 일반적인 예외를 일단 잡아서 그 유형을 나중에 판별한다
>>> try:
...     8/0
... except Exception, ex1:
...     'An error has occurred'
...
'An error has occurred'
>>> ex1
ZeroDivisionError('integer division or modulo by zero',)
>>> type(ex1)
<type 'exceptions.ZeroDivisionError'>
>>>

sys 꾸러미의 sys.exc_info()도 예외 유형과 메시지를 모두 제공한다. sys.exec.info()는 어떤 예외 유형이 던져질지 확실치 않은 상황에서 try-except 블록으로 어떤 코드를 감싼다면 매우 유용할 수 있다. 이 기술을 사용하는 예가 아래에 있다.

예제 7-10. sys.exc_info() 사용

# 명시적으로 예외 유형을 명명하지 않고 예외 처리를 수행
>>> x = 10
>>> try:
...     z = x / y
... except:
...     print "Unexpected error: ", sys.exc_info()[0], sys.exc_info()[1]
...
Unexpected error:  <type 'exceptions.NameError'> name 'y' is not defined

때로는 하나 이상의 예외를 잡기위해 사용할 수 있다. 이러한 예외 처리를해야하는 경우 파이썬은 다른 옵션 두 가지를 제공한다. 각기 다른 예외 상황마다 다른 작업들을 수행하고싶다면, 트릭을 수행하고 잘 작동하는 다양한 예외 구문을 사용할수 있다. 그런데 그것은 아마 코드가 길어질 것이다. 기타 원하는 옵션은 쉼표로 구분하고, 괄호로 예외 유형들을 감싼다. 예제 7-6을 사용하여 후자의 접근방식으로 표현한 아래 예제를 보라.

예제 7-11. 다중 예외 처리

# NameError 뿐만 아니라 방정식에 0이 사용된 경우에 ZeroDivisionError를 잡음
>>> try:
...     z = x/y
... except(NameError, ZeroDivisionError), err:
...     "An error has occurred, please check your values and try again"
...
'An error has occurred, please check your values and try again'

# 다수의 except 절을 사용
>>> x = 10
>>> y = 0
>>> try:
...     z = x / y
... except NameError, err1:
...     print err1
... except ZeroDivisionError, err2:
...     print 'You cannot divide a number by zero!'
...
You cannot divide a number by zero!

앞서 언급하였듯이, 예외는 파이썬의 클래스들 중 하나이다. 예외의 상위 클래스 및 하위 클래스가 있다. 하위 클래스 예외가 발생되는 예외 중 하나를 잡기 위해 상위클래스 예외를 잡을 수 있다. 예를 들어, 어떤 프로그램이 목록이나 사전 개체를 받아들이는 특정한 함수를 갖고 있다고 할 때, 따로 KeyError 나 IndexError를 찾는 것과 반대로 LookupError를 잡는 것이 이치에 맞다. 이것이 이루어지는 한 가지 방법에 대한 다음 예제를 살펴보도록 하자.

예제 7-12. 상위 클래스 예외 잡기

# 다음 예제에서, 우리는 몇 가지 컨테이너에서 값을 반환하는 함수를 정의한다.
# 함수는 목록이나 사전 개체 중 하나를 사용할 수 있다. 하위클래스인 KeyError과
# IndexError 를 각각 체크하는 것과 반대로 LookupError 상위클래스를 잡는다.
>>> def find_value(obj, value):
...     try:
...         return obj[value]
...     except LookupError, ex:
...         return 'An exception has been raised, check your values and try again'
...

# 먼저 사전과 목록을 만들고, 각 컨테이너에 존재하지 않는 값을 찾는 함수를 테스트
>>> mydict = {'test1':1,'test2':2}
>>> mylist = [1,2,3]
>>> find_value(mydict, 'test3')
'An exception has been raised, check your values and try again'
>>> find_value(mylist, 2)
3
>>> find_value(mylist, 3)
'An exception has been raised, check your values and try again'
>>>

만약 여러 예외 블록이 코딩되어있다면, 첫 번째 일치하는 예외가 잡힌다. 앞 예제에서 정의된 find_value 함수를 다시 설계해본다면, 각각 예외들이 일어나지 않고, 처음 일치하는 예외가 일어나면 다른 예외들은 무시된다. 이것이 어떻게 작동하는지 보자.

예제 7-13. 처음 일치하는 예외 잡기

# find_value() 함수를 재정의하여 각 예외를 개별적으로 검사
# 처음 일치하는 예외가 일어나면, 나머지는 무시된다.
# 따라서 이 예제에서는, LookupError 예외의 코드는 수행되지 않는다.
>>> def find_value(obj, value):
...     try:
...        return obj[value]
...     except KeyError:
...         return 'The specified key was not in the dict, please try again'
...     except IndexError:
...         return 'The specified index was out of range, please try again'
...     except LookupError:
...         return 'The specified key was not found, please try again'
...
>>> find_value(mydict, 'test3')
'The specified key was not in the dict, please try again'
>>> find_value(mylist, 3)
'The specified index was out of range, please try again'
>>>

try-except 블록은 원하는 만큼 겹칠 수 있다. 겹쳐진 예외 처리 블록의 경우, 만약 예외가 던져지면 오류를 받는 가장 안쪽의 블록으로부터 프로그램 제어가 바로 윗 블록으로 건너뛰게 된다. 이는 겹쳐진 루프 문에서 break 문을 만나면 실행을 멈추고 바깥 쪽 루프로 이동하는 것과 흡사하다. 다음은 그러한 논리을 보여주는 예제이다.

예제 7-14. 겹쳐진 예외 처리 블록

# 키보드 입력을 통하여 받은 숫자에 대하여 나눗셈을 수행
try:
    # 작업을 수행
    try:
        x = raw_input ('Enter a number for the dividend:  ')
        y = raw_input('Enter a number to divisor: ')
        x = int(x)
        y = int(y)
    except ValueError:
        # 예외를 처리하고 바깥의 try-except로 이동
        print 'You must enter a numeric value!'
    z = x / y
except ZeroDivisionError:
    # handle exception
    print 'You cannot divide by zero!'
except TypeError:
    print 'Retry and only use numeric values this time!'
else:
    print 'Your quotient is: %d' % (z)

앞의 예에서는, 서로 다른 예외 블록을 중첩시켰다. 만약 첫 ValueError가 발생하면, 바깥쪽의 예외 블록으로 제어를 넘긴다. 그러므로, ZeroDivisionError와 TypeError가 여전히 발생할 수 있다. 한편, 이 두 예외가 던져지지 않으면 else 절 내의 작업이 수행된다.

앞서 기술한 바와 같이, 자이썬에서는 자바 예외를 처리하는 것이 일반적이다. 때때로 자바 클래스에서 예외가 발생하게 될 것인데, 그것들은 파이썬의 예외들을 다루듯이 자이썬에서 처리하거나 표시할 수 있다.

예제 7-15. 자이썬에서 자바 예외 처리

// Java Class TaxCalc
public class TaxCalc {
    public static void main(String[] args) {
    double cost = 0.0;
    int pct   = 0;
    double tip = 0.0;
    try {
        cost = Double.parseDouble(args[0]);
        pct = Integer.parseInt(args[1]);
        tip = (cost * (pct * .01));
        System.out.println("The total gratutity based on " + pct + " percent would be " + tip);
        System.out.println("The total bill would be " + (cost + tip) );
    } catch (NumberFormatException ex){
        System.out.println("You must pass number values as arguments.  Exception: " + ex);
    } catch (ArrayIndexOutOfBoundsException ex1){
        System.out.println("You must pass two values to this utility.  " +
        "Format: TaxCalc(cost, percentage)  Exception: " + ex1);
    }
    }
}

위의 클래스를 자이썬에서 사용해보자.

# 자바 클래스 TaxCalc를 자이썬으로 가져와서 사용

>>> import TaxCalc
>>> calc = TaxCalc()

# TaxCalc에 문자열들이 담긴 목록을 전달하고, 자바 예외를 일으킨다.
>>> vals = ['test1','test2']
>>> calc.main(vals)
You must pass number values as arguments.  Exception:
java.lang.NumberFormatException: For input string: "test1"

# 이제 목록에 문자열로 숫자값을 전달하면, (반올림 문제를 제외하면) 예상대로 작동한다.
>>> vals = ['25.25', '20']
>>> calc.main(vals)
The total gratutity based on 20 percent would be 5.050000000000001
The total bill would be 30.3

자바의 예외를 자이썬으로 가져와서 사용하고, 던지는 것도 가능하다. 그저 파이썬의 예외를 던지듯이 할 수 있다.

예외 발생

때로 여러분은 자신만의 예외를 일으키고자 할 때가 있을 것이다. 프로그램에서는 특정한 키보드 입력값을 기대하고 있는데, 사용자는 엉뚱한 값을 입력하는 것과 같은 일이 벌어질 수 있다. 이런 경우가 여러분이 정의한 예외를 일으킬 수 있는 예가 될 수 있을 것이다. 여러분은 raise 문을 사용하여, 적절하다고 판단하는 위치에서 예외를 일으킬 수 있다. raise 구문을 사용하여 어떠한 파이썬 예외 유형이든 일으킬 수 있으며, 여러분이 스스로 정의한 예외(다음 절에서 이에 대하여 논의할 것이다)도 일으킬 수 있다. raise 문은 자바 언어에서의 throw 문과 유사하다. 자바에서는 필요에 따라 throw를 할 것인지 선택할 수 있다. 그런데, 자바에서는 메소드 내에서 try-catch를 사용하는 대신에 예외를 던져버리는 것이 가능한 경우에는 그 메소드에 throw 절을 적용할 수 있도록 허락한다. 파이썬에서는 raise 문을 그런 식으로 사용하는 것을 허락하지 않는다.

예제 7-16. raise 구문

raise ExceptionType 또는 String[, message[, traceback]]

위의 구문과 같이, raise를 사용하여 오류를 일으킬 때에 창의성을 발휘하여 여러분만의 문자열을 사용할 수 있다. 그러나, 그것이 최선의 방법은 아니며 가능하다면 정의된 예외를 일으키려고 노력해야 한다. 여러분은 또한 오류를 설명하는 짧은 메시지를 제공할 수 있다. 이 메시지는 임의의 문자열이 될 수 있다. 예제를 잠깐 살펴보자.

예제 7-17. 메시지를 사용한 예외 발생

>>> raise Exception("An exception is being raised")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: An exception is being raised
>>> raise TypeError("You've specified an incorrect type")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: You've specified an incorrect type

지금 여러분은 파이썬 번역기에서 일으킨 몇몇 예외를 보았다. 예외가 일어날 때마다, 예외와 코드에서 문제를 일으키는 행에 대한 피드백을 제공하기 위하여 번역기가 생성한 메시지가 나타난다. 예외가 일어날 때에는 항상 역추적(traceback) 부분이 있다. 이것이 예외가 일어난 곳에 대하여 정말 많은 정보를 제공한다. 끝으로, 다른 형식을 사용하여 예외를 일으키는 것을 살펴보자. 즉, raise Exception, “message” 형식을 사용할 수 있다.

예제 7-18. 예외와 메시지 구문을 함께 사용하는 raise 문

>>> raise TypeError,"This is a special message"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: This is a special message

자신만의 예외를 정의

파이썬에서는 예외 클래스를 생성함으로써 자신만의 예외를 정의할 수 있다. 기본이 되는 Exception 클래스로부터 상속받는 클래스를 정의하면 된다. 가장 쉬운 방법으로는 클래스에 pass 문을 사용할 수 있을 것이다. 예외 클래스들은 초기화 메소드를 사용하여 매개변수를 받아들일 수 있으며 __str__ 메소드를 사용하여 예외를 반환한다. 어떤 예외를 작성하더라도 메시지를 받아들이도록 해야 한다. 예외가 어떤 종류의 오류를 지칭하는 경우에는 예외 이름이 Error로 끝나도록 하는 것이 좋은 습관이다.

예제 7-19. 기본 예외 클래스 정의

class MyNewError(Exception):
    pass

이 예제는 여러분이 생성할 수 있는 가장 단순한 형태의 예외이다. 이 예외는 여느 다른 예외와 마찬가지로 발생시킬 수 있다.

raise MyNewError("Something happened in my program")

보다 involve된 예외 클래스는 다음과 같이 작성할 수 있을 것이다.

예제 7-20. 초기화 함수를 사용한 예외 클래스

class MegaError(Exception):
    """ This is raised when there is a huge problem with my program"""
    def __init__(self, val):
        self.val = val
    def __str__(self):
        return repr(self.val)

경고하기

경고는 프로그램에서 언제든지 올릴 수 있고, 경고 메시지의 일부 유형을 표시하는 데 사용할 수 있다. 하지만 반드시 실행을 중단하지는 않는다. 어떤 메서드를 앞으로는 사용하지 않도록 권고하되, 호환성을 위해 계속 작동할 수 있도록 하는 것이 좋은 예이다. 그러한 메서드 대신에 다른 것을 사용하기를 권장하지만 프로그램의 실행이 멈추지는 않음을 사용자에게 알리기 위하여 경고를 생성할 수 있다. 경고를 정의하기는 쉽지만, 필터를 사용하여 경고들에 대한 규칙을 정의하려는 경우에는 복잡할 수도 있다. 경고 필터는 특정 경고의 동작을 수정하는 데 사용된다. 많은 예외들과 같은, 정의된 많은 경고들은 분류로 나눠서 사용할 수 있다. 이러한 경고를 예외로 쉽게 변환할 수 있도록 하기 위해, 경고들은 예외 타입의 모든 인스턴스이다. 예외들은 에러가 아니라, 경고나 메세지라는 사실을 기억하라. 예를들어, StopIteration 예외는 프로그램의 에러를 표시하는 프로그램이 아닌 루프의 반복을 중지하는 프로그램에 의해 발생된다.

경고를 발행하려면, 먼저 여러분의 프로그램으로 warnings 모듈을 가져와야 한다. 그 다음으로는, 간단히 warning.warn() 함수 호출을 만들고, 경고 메세지 문자열을 전달하면 된다. 발행 경고의 종류를 제어하고 싶은 경우에는, 경고 클래스를 전달할 수 있다. 표 7.2 는 경고 목록이다.

예제 7-21. 경고 발생

# 항상 warnings 모듈을 먼저 들여올 것
import warnings

# 경고를 설정하는 두 가지 예
warnings.warn("this feature will be deprecated")
warnings.warn("this is a more involved warning", RuntimeWarning)

# 함수에서 경고 사용

# 다음 함수의 사용이 금지되었다고 가정하고, 함수의 사용자에게 경고할 수 있다

# 다음의 함수는 주어진 일수를 현재 년도에 더한 년도를 계산한다.
# 물론, 이것은 Y2K 이전의 코드이므로 사용이 금지된다.
# 3000년 즈음에는 이 코드를 사용해서는 안 될 것이다!
>>> def add_days(current_year, days):
...     warnings.warn("This function has been deprecated as of version x.x", DeprecationWarning)
...     num_years = 0
...     if days > 365:
...         num_years = days/365
...     return current_year + num_years
...

# 이 함수를 호출하면 경고를 반환하기는 하지만, 오류를 일으키지는 않으며 기대하는 결과를 여전히 반환한다.
>>> add_days(2009, 450)
__main__:2: DeprecationWarning: This function has been deprecated as of version x.x
2010

표 7-2. 파이썬 경고 분류

경고 설명
Warning 최상위 경고 클래스
UserWarning 사용자 정의 경고
DeprecationWarning 사용금지된(deprecated) 기능에 대한 경고
SyntaxWarning 구문 문제
RuntimeWarning 실행시간 문제
FutureWarning 앞으로의 릴리스에서 변경될 예정인 특정 기능에 대한 경고

warnings 모듈을 코드에 포함시킴으로써 내장된 여러 함수를 사용할 수 있다. 경고를 걸러내고 그 행위를 변경하고자 한다면 필터를 생성하면 된다. 표 7-3에 warnings 모듈에 딸려오는 함수들을 나열하였다.

표 7-3. 경고 함수

함수 설명
warn(message[, category[, stacklevel]]) 경고를 발행. 매개변수는 메시지 문자열, 선택적인 경고 카테고리, 그리고 호출하는 함수 혹은 함수 자체 중 어느 스택 프레임으로부터 경고가 발생하였는지를 알려주는 선택적인 스택 레벨을 포함한다.
warn_explicit(message, category, filename, lineno[, module[, registry]]) 이것은 보다 세부적인 경고 메시지를 제공하며 category를 필수 파라미터로 만든다. filename, lineno 및 module은 경고가 어디에 위치하는지 알려준다. registry는 현재 활성화된 모든 경고 필터를 나타낸다.
showwarning(message, category, filename, lineno[, file]) 경고를 파일에 기록할 수 있도록 해준다.
formatwarning(message, category, filename, lineno) 경고에 대한 서식화된 문자열을 생성한다.
simplefilter(action[, category[, lineno[, append]]]) 경고 필터에 대한 정렬된 목록에 간단한 엔트리를 삽입한다. simplefilter는 카테고리와 행 번호가 일치하는 한 어떤 모듈의 어떤 메시지든 항상 match하므로 정규표현식을 필요로 하지 않는다. 아래에서 설명할 filterwarnings()는 정규표현식을 사용하여 경고를 match한다.
resetwarnings() 모든 경고 필터를 리셋.
filterwarnings(action[, message[, category[, module[, lineno[, append]]]]]) 경고 필터 목록에 엔트리를 추가한다. 경고 필터는 경고의 동작을 수정할 수 있도록 해준다. 경고 필터의 action은 표 7-4에 나열된 것 중 하나가 될 수 있고, message는 정규표현식, category는 발행될 경고의 종류, module은 정규표현식이 될 수 있고, lineno는 모든 행에 대하여 일치하는 행 번호이고, append는 필터가 모든 필터의 목록에 추가되어야할 지를 규정한다.

표 7-4. 파이썬 필터의 역할

필터 역할
‘always’ 경고 메시지를 항상 출력
‘default’ 경고가 발생하는 곳마다 한 번 씩 출력
‘error’ 경고를 예외로 변환
‘ignore’ 경고를 무시
‘module’ 경고가 발생하는 모듈마다 한 번 씩 경고
‘once’ 단 한 번만 경고

다음 예제를 통하여 경고 필터를 사용하는 몇 가지 방법을 알아보자.

예제 7-22. 경고 필터 예제

# 경고에 대한 예외를 일으키기 위한 간단한 경고 필터를 설정

>>> warnings.simplefilter('error', UserWarning)
>>> warnings.warn('This will be raised as an exception')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Applications/Jython/jython2.5.1rc2/Lib/warnings.py", line 63, in warn
    warn_explicit(message, category, filename, lineno, module, registry,
  File "/Applications/Jython/jython2.5.1rc2/Lib/warnings.py", line 104, in warn_explicit
    raise message
UserWarning: This will be raised as an exception

# resetwarnings()를 사용하여 모든 활성화된 필터를 끄기
>>> warnings.resetwarnings()
>>> warnings.warn('This will not be raised as an exception')
__main__:1: UserWarning: This will not be raised as an exception

# 경고를 거르기 위하여 정규 표현식을 사용
# 여기서는, “one”이라는 단어를 포함하는 모든 경고를 무시
>>> warnings.filterwarnings('ignore', '.*one*.',)
>>> warnings.warn('This is warning number zero')
__main__:1: UserWarning: This is warning number zero
>>> warnings.warn('This is warning number one')
>>> warnings.warn('This is warning number two')
__main__:1: UserWarning: This is warning number two
>>>

다른 많은 경고 필터가 쓰이며, 각각은 filterwarnings() 함수를 호출하여 순서가 있는 목록에 다른 경고를 추가한다. 특정 경고가 목록의 각 필터 명세에 맞는 것을 찾을 때까지 맞춰보게 된다. 현재 사용되는 필터를 확인하기 위해서는 print warnings.filters 명령을 내리면 된다. 명령행에서 -W 옵션을 사용함으로써 경고 필터를 지정할 수도 있다. 끝으로, resetwarnings() 함수를 사용하여 모든 경고를 리셋할 수도 있다.

명령행 인자를 사용하여 경고 필터를 설정하는 것도 가능하다. 스크립트 또는 모듈에 대한 경고를 걸러내고자 할 때 유용할 것이다. 예를 들어, 스크립트 기반으로 경고를 거르고자 한다면 스크립트를 호출할 때 -W 명령행 인자를 사용할 수 있다.

예제 7-23. -W 명령행 옵션

-Waction:message:category:module:lineno

예제 7-24. W 명령행 옵션 사용 예제

# 다음의 test_warnings.py 스크립트를 명령행에서 수행하려 한다고 가정
import warnings
def test_warnings():
    print "The function has started"
    warnings.warn("This function has been deprecated", DeprecationWarning)
    print "The function has been completed"

if __name__ == "__main__":
    test_warnings()

# 다음의 구문을 사용하여 어떠한 경고도 필터링하지 않고 자이썬을 시작
jython test_warnings.py
The function has started
test_warnings.py:4: DeprecationWarning: This function has been deprecated
  warnings.warn("This function has been deprecated", DeprecationWarning)
The function has been completed

# 스크립트를 실행하고 모든 deprecation 경고를 무시
jython -W "ignore::DeprecationWarning::0" test_warnings.py
The function has started
The function has been completed

# 스크립트를 마지막으로 한번 실행하고 DeprecationWarning을 예외로서 조치
# As you see, it never completes
jython -W "error::DeprecationWarning::0" test_warnings.py
The function has started
Traceback (most recent call last):
  File "test_warnings.py", line 8, in <module>
    test_warnings()
  File "test_warnings.py", line 4, in test_warnings
    warnings.warn("This function has been deprecated", DeprecationWarning)
  File "/Applications/Jython/jython2.5.1rc2/Lib/warnings.py", line 63, in warn
    warn_explicit(message, category, filename, lineno, module, registry,
  File "/Applications/Jython/jython2.5.1rc2/Lib/warnings.py", line 104, in warn_explicit
    raise message
DeprecationWarning: This function has been deprecated

경고는 상황에 따라서는 매우 유용할 수 있다. 필요에 따라서 간단하게 혹은 복잡하게 만들 수 있다.

단정과 디버깅

파이썬에서는 단정문assert statement을 사용함으로써 디버깅을 쉽게 할 수 있다. Cpython에서는 __debug__ 변수도 사용할 수 있지만, 자이썬에 대해서는 번역기를 위한 최적화 모드를 제공하지 않기 때문에 현재 사용하지 않는 기능이다. 단정문이란 코드의 특정 부분이 예상을 벗어났음을 가리키게끔 출력할 수 있는 구문이다. 단정문은 표현에 대한 참 또는 거짓 값을 확인하여, 거짓으로 밝혀질 경우 AssertionError와 함께 부가적인 메시지를 발행한다. 표현이 참으로 평가되는 경우에는 그 단정문은 완전히 무시된다.

assert 표현식 [, 메시지]

단정문을 효과적으로 사용함으로써, 어떠한 에러든 쉽게 잡아낼 수 있으며, 보다 쉽게 디버깅을 해나갈 수 있다. 예제 7-25는 단정문의 사용법을 보여준다.

예제 7-25. assert 사용

# 다음은 단정문이 어떻게 평가되는지 보여주는 예제이다.
>>> x = 5
>>> y = 10
>>> assert x < y, "The assertion is ignored"
>>> assert x > y, "The assertion raises an exception"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: The assertion raises an exception

# 파라미터를 검증하기 위하여 단정문을 사용한다
# 각 파라미터가 정수인지를 검사해보자
>>> def add_numbers(x, y):
...     assert type(x) is int, "The arguments must be integers, please check the first argument"
...     assert type(y) is int, "The arguments must be integers, please check the second argument"
...     return x + y
...
# 함수를 사용할 때, 필요에 따라 AssertionError가 일어난다
>>> add_numbers(3, 4)
7
>>> add_numbers('hello','goodbye')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in add_numbers
AssertionError: The arguments must be integers, please check the first argument

문맥 관리자

파일이나 데이터베이스 접속과 같은 자원을 관리하기 위한 코드가 적절하게 쓰여졌는지 확실히 하는 것은 중요한 주제이다. 만약에 파일 또는 데이터베이스 접속을 열어둔 채로 닫지 않는다면 우리의 프로그램은 문제를 일으킬 것이다. 때로는 개발자가 그러한 자원을 적절히 관리하고자 try-finally 블록을 사용하기로 결정하곤 한다. 이것은 자원 관리를 위해 받아들여질 만한 방법이기는 하나, 때로는 오용되어서 프로그램에서 예외가 일어날 때에 문제를 일으키는 수가 있다. 예를 들어, 데이터베이스 접속을 가지고 작업을 할 때, 접속을 연 이후에 예외가 발생하면, 현재 블록의 프로그램 제어가 깨져서 이후의 처리를 모두 건너뛰게 된다. 그러한 경우에는 접속을 결코 닫을 수 없게 된다. 이러한 점에서 자이썬의 새로운 기능인 문맥 관리자의 개념이 중요성을 띠게 된다. with 문을 사용한 문맥 관리자는 자이썬 2.5에 새로 나타난 것으로, 자원이 예상대로 관리될 것임을 확실히 할 수 있는 훌륭한 방법이다.

with 문을 사용하기 위해서는 __future__로부터 import하여야 한다. with 문은 기본적으로 자원 관리에 대하여 걱정할 필요 없이 개체를 취하여 사용할 수 있도록 해준다. 예를 들어, 시스템에 있는 파일을 열어서 몇 줄을 읽는다고 하자. 파일에 대한 작업을 수행하려면 우선 파일을 열고, 어떤 처리나 파일 내용 읽기를 수행한 다음, 자원을 반납하기 위하여 파일을 닫는다. with 문을 사용한 문맥관리자는 파일을 열어서 작업하는 일을 간결한 구문으로 단순화시켜준다.

예제 7-26. 파이썬 with 구문의 예

#  players.txt라는 파일로부터 읽기
>>> from __future__ import with_statement
>>> with open('players.txt','r') as file:
...     x = file.read()
...
>>> print x
Sports Team Management
---------------------------------
Josh – forward
Jim – defense

위의 예제에서 우리는 파일을 닫는 것에 대하여 걱정할 필요가 없는데, 이는 문맥 상에서 이루어지기 때문이다. 이는 문맥 관리 규약을 확장한 개체에 대해서도 동작한다. 달리 말해, 어떠한 개체든지 __enter__()__exit__()라는 두 메소드를 구현하면 이는 문맥 관리 규약을 준수하는 것이다. with 문이 시작될 때는, __enter__() 메소드가 실행된다. 마찬가지로, with 구문이 끝날 때는 마지막으로 __exit__() 메소드가 수행된다. __enter__() 메소드는 인자를 취하지 않는 반면, __exit__() 메소드는 형, 값, 역추적의 세 인자를 선택적으로 취한다. __exit__() 메소드는 예외가 던져졌는지의 여부를 나타내는 True 및 False 값을 반환한다. with 구문 내의 as 변수를 사용하여 코드 블록 내에서 개체를 사용할 수도 있다. lock과 같은 자원을 사용한다면 부가적인 구절은 필요하지 않을 것이다.

문맥 관리 규약을 따른다면, 이러한 기법을 사용할 수 있는 여러분의 개체를 생성하는 것이 가능하다. 필요하다면 어떤 개체든지 __enter__() 메소드에서 생성할 수 있다.

예제 7-27. 문맥 관리 규약을 따르는 간단한 개체를 생성

# In this example, my_object facilitates the context management protocol
# as it defines an __enter__ and __exit__ method
class my_object:
    def __enter__(self):
        # Perform setup tasks
        return object

    def __exit__(self, type, value, traceback):
        # Perform cleanup

변경 불가능한 개체에 대하여 작업한다면 __enter__() 메소드의 수정을 위해 그 개체에 대한 사본을 만들 필요가 있을 것이다. 반면 __exit__() 메소드는 False를 반환하므로 정리 작업이 필요한 것이 아니라면 생략해도 무방하다. 문맥 관리자에서 예외가 발생하면 대표하는 형, 값, 역추적의 세 개의 인자와 함께 __exit__()가 호출된다. 그렇지만, 예외가 일어나지 않으면 __exit__()에는 세 개의 None 인자가 주어진다. 만약 __exit__()가 참을 반환하면, 예외는 “먹혀”버리거나 무시되며, with 문의 다음에 오는 구문을 이어서 수행한다.

요약

이번 장에서, 우리는 파이썬 어플리케이션에서의 예외 및 예외 처리에 대한 여러 주제에 대하여 논의하였다. 먼저, try-except-finally 코드 블록의 예외 처리 구문과 그에 대한 사용법을 배웠다. 다음으로는 예외를 만들어내는 것이 왜 중요한지, 또한 어떻게 해야하는지를 배웠다. 그 주제는 예외를 어떻게 정의하는지에 대한 논의로 이어져서, 우리는 그를 위해서는 예외 유형의 개체를 확장하는 클래스를 정의하는 것이 반드시 필요함을 배우게 되었다.

예외에 대하여 배운 다음, 우리는 경고 framework로 들어가서 그것을 어떻게 사용하는지 논의했다. 코드가 사용 금지deprecate될 수 있어 사용자에게 경고하고 싶지만 예외를 일으키고 싶지는 않을 때와 같은 경우에 warnings를 사용할 수 있음이 중요할 것이다. 그 다음 주제는 단정으로, 단정문이 어떻게 우리가 프로그램을 디버그하는 것을 돕는지 살펴보았다. 끝으로, 자이썬 2.5에서 새로이 도입된 문맥 관리자 및 with 문의 사용을 건드려보았다.

다음 장에서는 보다 큰 규모의 프로그램을 탐구하고, 모듈과 패키지에 대하여 배우도록 하겠다.