파이썬 예외처리

파이썬 예외처리

파이썬 프로그래밍을 하면서 각종 외부에서 들어오는 데이터나 프로그램 내부에서 생성된 데이터를 다루다 보면 어느덧 각종 파이썬이 발생시키는 에러에 마주하게 되는 자신을 발견하게 될 것입니다. 자신의 실수건, 외부에서 들어오는 데이터가 이상하건 프로그램은 각종 상황에서도 잘 동작해야 합니다. 열쇠는 바로 파이썬의 예외처리입니다.

파이썬 예외처리
대표적인 좋은 에러 케이스

파이썬 자료형의 ‘문자열 및 시퀀스 연산 기초’ 편에서 인덱스 연산에 대해서 배웠습니다. 기억을 한 번 떠올려 보죠.

>>> L = 'Hello'
>>> L[5] # 위 문자열의 5번째 인덱스에 접근합니다.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

불행히도 ‘Hello’의 인덱스 유효범위는 0부터 4까지 입니다. 5는 문자열의 인덱스 범위를 넘어버리죠. 파이썬은 이 경우 IndexError 를 발생시킵니다. 아, 이건 실행해보면 잡을 수 있는 에러라구요? 그렇다면 우리가 이런 API(Application Programming Interface)를 만든다고 생각해볼까요? 아래는 파이썬으로 만들어진 마이크로 프레임워크(Micro framework)인 Flask 예제 입니다.

from flask import abort

my_list = ['Gingerbread', 'Honeycomb', 'Ice-cream Sandwich' , 'Jellybean']

@app.route('/hello/api/v1.0/<int:id>', methods=['GET'])
def get_some(id):
    return my_list[id]

이 API 는 “http://www.example.com/hello/api/v1.0/” 뒤에 숫자가 오면 my_list에서 해당 인덱스를 반환하는 간단한 API 입니다. “http://www.example.com/hello/api/v1.0/1″을 호출하면 ‘Gingerbread’ 문자열을 리턴합니다. ‘역시 난 천재야’ 라고 생각하셨다면, “http://www.example.com/hello/api/v1.0/10″을 입력하면 어떻게 될까요?

500 Internal Server Error

인덱스 10은 리스트의 인덱스 범위를 초과했기 때문에 에러가 발생합니다. 따라서 서버에서 에러가 발생했기 때문에 500을 반환하는 것이지요. 뿐만 아니라 당연히 있어야 하는 파일을 읽으려 보니 막상 해당 경로에 없다거나, 어쩌다보니 어떤 숫자를 0으로 나누게 된다거나… 이런 일이 비일비재하게 일어납니다. 세상일이니까요. 그래서 우리는 다양하게 일어날 수 있는 각종 에러를 처리해야 합니다. 이를 예외처리라 합니다.

왜 예외처리를 해야하는가

예외 처리를 해야하는 이유는 위에서 설명한 에러 처리뿐만 아니라 몇 가지 이유가 더 있습니다.

에러 처리

파이썬은 프로그램 실행 중 에러를 발견했을 때 즉각 예외(exception)를 발생(raise)시킵니다. 프로그래머는 이 예외를 처리하거나 그냥 무시할 수 있습니다. 만약 예외를 무시한다면 파이썬의 기본 에러 처리 방식으로 예외를 처리합니다. 즉 앞에서 보았던 예제처럼 프로그램을 정지시키고 에러 메시지를 출력하는 것입니다. 만약 이런 파이썬의 기본 에러처리 방식을 (당연히…)사용하고 싶지 않으면 프로그래머가 별도로 예외를 처리할 수 있습니다. 그러면 파이썬은 파이썬의 기본 에러 처리 방식 대신 프로그래머가 작성한 예외처리 코드를 실행합니다. 그 코드에 프로그램을 정지시키는 명령어만 없다면 예외가 발생해도 파이썬은 프로그램을 정지시키지 않고 계속 실행할 것입니다.

에러 알림

예외 발생은 비정상적인 상태를 알리는데 유용합니다. 문자열을 반환하는 함수 A가 있다고 가정합니다. A의 반환 값은 문자열인데 만약 함수 내부적으로 연산이 실패했다면 함수 A를 호출한 호출자(caller)에게 어떻게든 문제가 발생했다고 알려야 합니다. 물론 빈 문자열을 반환할 수도 있겠지만, 빈 문자열도 호출자가 정상적인 값으로 인식할 우려가 있습니다. 이 경우에는 예외를 발생시키는 방식이 훨씬 깔끔합니다.

간결해지는 코드 흐름

잠시 후 소개하겠지만 finally 구문은 try 구문 내에서 예외가 발생하건 발생하지 않건 반드시 실행됩니다. 따라서 어떠한 자원(네트워크, 파일 등)을 사용하고 반드시 그 뒷처리를 해야할 때 코드 흐름을 간결하게 가져갈 수 있습니다.

깔끔한 코드

C 언어로 작성된 오픈소스 프로젝트의 코드를 살펴보면 약간의 goto 구문을 발견할 수 있습니다. 거의 대부분이 에러 상황에서 에러 처리 루틴으로 건너뛰기 위해서 인데요, 파이썬의 예외처리는 이런 goto 를 사용하지 않아도 되기 때문에, 그리고 goto 구문이 없기에 예외로 처리하면 코드가 매우 깔끔해집니다. 뭐 , 이건 부수적인 효과겠죠? : )

파이썬 예외처리 방법

도입부에서 설명한 코드는 경각심을 일깨우기 위해 만든 flask 코드라, 이번에는 파이썬 함수를 만들어 보겠습니다.

# exception_test.py
my_list = [1, 2, 3, 4, 5]
def exception_test():
return my_list[5]

이 코드를 쉘에서 실행시켜 보겠습니다.

$ python exception_test.py
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

위 IndexError는 시퀀스 자료형의 인덱싱 연산에서 범위를 벗어난 아이템을 가리킬 때 발생하는 매우 흔한 에러입니다. my_list 의 5번, 즉 6번째 아이템은 존재하지 않는데 이 아이템에 접근하려 하기 때문에 발생하는 에러지요. 만약 위와 같은 코드가 API 내에 들어가 있다면 사용자는 수시로 ‘500 Internal Server Error’ 와 마주하게 될 것입니다. 이를 파이썬의 예외처리를 사용하여 좀 더 안전하게 바꿔보겠습니다.

파이썬의 예외처리 문법은 아래와 같습니다.

try:
.....
except [예외 종류 [as 예외 변수]] :

....

문법대로 except 는 세 가지 방법으로 사용할 수 있습니다.

  1. except 만 사용: try 내에서 발생한 모든 예외를 처리하겠다는 의미
  2. except ‘예외 종류’ 사용: try 내에서 발생한 예외 중 ‘예외 종류’에 해당하는 예외만 처리하겠다는 의미
  3. except ‘예외 종류’ as ‘예외 변수’ 사용 : try 내에서 발생한 예외 중 ‘예외 종류’에 해당하는 예외만 처리하며, 이 예외에 대한 상세 정보를 다뤄야 할때 사용

예외 처리 방법 예제

아래 코드는 1번에 해당하는 코드입니다. (윈도우즈 Python 2.7 상에서 테스트)

# exception_test1.py
import shutil
try:
    shutil.rmtree('non_exist_dir')
except:
    print 'Raise an exception, but continuing...'

아래 코드는 2번에 해당하는 코드입니다. WindowsError 예외만 처리하도록 코드를 작성했습니다.

# exception_test2.py
import shutil
try:
    shutil.rmtree('non_exist_dir')
except WindowsError:
    print e
    print 'Raise an exception, but continuing...'

실행하면

$ python exception_test1.py
[Error 3] : 'non_exist_dir/*.*'
Raise an exception, but continuing...
$ python exception_test2.py
[Error 3] : 'non_exist_dir/*.*'
Raise an exception, but continuing...

1, 2 번 모두 결과는 동일합니다. 그런데 Error 3 이 뭘까요? 세부 내용을 확인하고 싶을 때 발생한 예외 정보를 변수로 받아서 살펴봅니다.

#exception_test3.py
import shutil
import ctypes
try:
    shutil.rmtree('non_exist_dir')
except WindowsError as e:
    print e.errno
    print ctypes.FormatError(e.errno)

대충 짐작은 가지만 정확하게 무엇이 원인인지 알고 싶다면 WindowsError를 변수 e 로 받아서 무엇이 원인인지 출력해봅니다.

 $ python exception_test3.py
2
지정된 파일을 찾을 수 없습니다.

그렇군요! WindowsError 에서 errno 가 2라는 것을 확인했고, rmtree()에 인자로 넘겨준 디렉토리가 존재하지 않는 다는 사실을 알 수 있습니다.
except 는 중첩해서 사용할 수 있는데, try 구문 내에서 예외를 발생시킬 수 있는 구문이 2개 이상일 때 유용합니다.

import shutil
my_list = [1, 2, 3, 4, 5]
try:
    n = my_list[5]
    shutil.rmtree('non_exist_dir' + str(n))
except WindowsError as e:
    ....
except IndexError as e1:
    ....

여러 개의 예외 한 번에 처리하기

예외마다 따로 처리하는게 의미없는 코드라면, 이를 한 번에 묶어서 처리할 수 있습니다. 그냥 except 만 사용하면 try 내에서 발생한 모든 예외를 처리하겠다는 의미입니다.

try:
    something_wrong()
except:
    ....

아니면 일부 예외만 묶어서 처리하고 싶다면 아래와 같이 튜플로 처리해도 됩니다.

try:
    something_wrong()
except ValueError:
    ....
except (KeyError, IndexError, WindowsError):
    ....

어때요, 참 쉽죠?

파이썬 예외처리 참 쉽죠?
어때요, 참 쉽죠?

else와 finally

이제 else와 finally 를 배워볼 차례인데요, else 는 오직 if와 짝을 이루는 줄 알았더니 의외로 try 와도 짝을 이룰 수 있습니다. try~except 에 else 를 사용하면 예외가 발생하지 않을 경우 try 내의 코드가 실행된 후 else 에서 정의한 코드가 실행됩니다.

 # exception_test3.py
L = [1,2,3]
def func1():
  try:
    num = L[3]
  except IndexError:
    print 'IndexError'
  else:
    print 'Keep calm and go ahead'

def func2():
  try:
    num = L[1]
  except IndexError:
    print 'IndexError'
  else:
    print 'Keep calm and go ahead'</pre>
func1()
func2()

결과는 아래와 같습니다. func1()은 try 내에서 예외가 발생했기 때문에 except 내의 구문의 실행되고 끝났구요, func2()는 예외가 발생하지 않았기 때문에 else 내의 구문이 실행되었습니다.

 $ python exception_test3.py
IndexError
Keep calm and go ahead

finally는 예외가 발생하건 발생하지 않건 무조건 마지막에 실행됩니다.

예외가 발생하지 않았고 else와 finally 가 모두 있다면 둘 다 실행됩니다.

예외를 발생시키자 raise

지 금까지 예외 처리 방법을 배웠는데, 이번에는 예외를 발생시키는 방법에 대해서 설명하겠습니다. 예외 처리 방법에 대해 설명하다가 예외 발생 방법에 대해서 말하려니 좀 앞뒤가 안맞는 것 같지만, 예외를 발생시키는 부분은 반드시 필요합니다. 특히 다른 프로그램에 사용되는 라이브러리에서는 꼭 필요한 존재입니다.

예외를 발생시키기 위한 문법은 3가지가 있습니다.

첫 번째, exception 클래스를 상속한 클래스의 인스턴스를 이용해서 예외를 발생시킵니다. exception 클래스를 상속한 클래스는 앞서 설명한 IndexError, ValueError 등이 있으며, 이후 설명할 사용자 정의 예외도 해당됩니다.

raise ValueError() # ValueError를 생성 후 예외를 발생시켰습니다.

두 번째, exception 클래스 자체를 이용합니다.

raise ValueError 

위 예제는 클래스 ValueError 만 적었는데, 암시적으로 ValueError의 인스턴스가 내부에서 생성 후 이를 이용합니다.

세 번째, 그냥 최근에 발생한 예외를 다시 발생시킵니다.

try:
raise ValueError()
except ValueError:
raise

except 내부의 raise 는 ValueError 예외를 발생시키게 됩니다.

사용자 정의 예외

작업 중입니다.

참고자료

  1. 파이썬 공식문서

 

 

Leave a Reply

Leave a Reply

Your email address will not be published. Required fields are marked *