에러와 예외처리

이 노트북은 제이크 반더플라스(Jake VanderPlas)의 A Whirlwind Tour of Python(OReilly Media, 2016)를 기반으로 만들어졌습니다. 이 내용은 CC0 라이센스를 따릅니다. 전체 노트북의 목록은 https://github.com/rickiepark/WhirlwindTourOfPython 에서 볼 수 있습니다.

< 함수 | 목차 | 반복자 >

 

프로그래머로서의 기술에 상관없이 코딩 실수는 하기 마련입니다. 여기에는 세 종류의 실수가 있습니다:

  • 문법 에러: 파이썬 문법에 맞지 않는 코드로 인한 에러(일반적으로 고치기 쉽습니다)
  • 실행 에러: 문법적으로는 옳지만 실행시에 나는 에러, 아마도 잘못된 입력 때문입니다(고치기 쉬운 편입니다)
  • 논리 에러: 문제없이 코드가 실행되지만 원치않는 결과가 나옵니다(종종 고치기 매우 어렵습니다)

여기에서는 어떻게 실행시 에러를 깔끔하게 다루는지 배웁니다. 파이썬은 예외처리 핸들링 프레임워크로 실행시 에러를 처리합니다.

실행 에러

파이썬 코딩을 해보았다면 실행 에러를 만난 적이 있을 것입니다. 이 에러는 다양한 곳에서 일어 납니다.

예를 들어, 정의되어 있지 않은 변수를 참조할 때 발생합니다:

print(Q)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-1-cbf1bd89097d> in <module>()
----> 1 print(Q)


NameError: name 'Q' is not defined

또는 정의되지 않은 연산을 시도할 때입니다:

1 + 'abc'
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-2-a51a3635a212> in <module>()
----> 1 1 + 'abc'


TypeError: unsupported operand type(s) for +: 'int' and 'str'

또는 수학적으로 계산할 수 없는 연산을 시도할 때입니다:

2 / 0
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-3-8b4ac6d3a3e1> in <module>()
----> 1 2 / 0


ZeroDivisionError: division by zero

또는 존재하지 않는 원소에 접근할 때입니다:

L = [1, 2, 3]
L[1000]
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-4-354067ebdc84> in <module>()
      1 L = [1, 2, 3]
----> 2 L[1000]


IndexError: list index out of range

각 경우에 파이썬은 단순히 에러가 일어난 것을 알려주는 것 뿐만 아니라 정확히 무엇이 잘못 되었는지 에러가 발생한 코드의 위치는 어디인지의 정보를 포함한 의미있는 예외를 발생시킵니다. 이런 에러의 의미를 사용하면 코드에 있는 문제를 찾아가는데 매우 도움이 됩니다.

예외 처리: try와 except

실행시 예외를 다루기 위한 도구는 tryexcept 절입니다. 기본 구조는 다음과 같습니다:

try:
    print("먼저 이 라인이 실행됩니다")
except:
    print("에러가 발생할 때 실행됩니다")

먼저 이 라인이 실행됩니다

첫 번째 블럭에서 에러가 발생하지 않았기 때문에 두 번째 블럭은 실행되지 않습니다. try 블럭안에 잘못된 코드를 넣으면 어떻게 되는지 보겠습니다:

try:
    print("다음 코드를 실행합니다:")
    x = 1 / 0 # ZeroDivisionError
except:
    print("무언가 잘못되었습니다!")
다음 코드를 실행합니다:
무언가 잘못되었습니다!

try 문 안에서 에러가 발생하면(여기서는 ZeroDivisionError) 캐치되어 except 문이 실행됩니다.

함수나 코드에서 사용자의 입력을 체크하는데 자주 사용하는 방법입니다.  예를 들어 0으로 나누는 에러를 캐치하여 10^{100} 와 같이 아주 충분히 큰 값을 반환하는 함수를 만들 수 있습니다:

def safe_divide(a, b):
    try:
        return a / b
    except:
        return 1E100
safe_divide(1, 2)
0.5
safe_divide(2, 0)
1e+100

이 함수에는 교묘한 문제가 있습니다. 다른 종류의 예외가 발생하면 어떻게 될까요? 가령, 다음은 예상하지 못한 상황입니다:

safe_divide (1, '2')
1e+100

정수와 문자열을 사용해 나눗셈을 하면 TypeError가 발생됩니다. 이 에러도 과도하게 캐치하여 ZeroDivisionError로 간주하는 셈이 됩니다! 이런 이유 때문에 명시적으로 캐치할 예외를 지정하는 것이 좋습니다:

def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return 1E100
safe_divide(1, 0)
1e+100
safe_divide(1, '2')
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-13-cbb3eb91a66d> in <module>()
----> 1 safe_divide(1, '2')


<ipython-input-11-57f0d324952e> in safe_divide(a, b)
      1 def safe_divide(a, b):
      2     try:
----> 3         return a / b
      4     except ZeroDivisionError:
      5         return 1E100


TypeError: unsupported operand type(s) for /: 'int' and 'str'

이제 0으로 나누는 에러만 캐치하고 나머지 에러는 그냥 통과시킵니다.

예외 발생: raise

파이썬을 사용할 때 예외에 담긴 정보가 얼마나 유용한지 보았습니다. 여러분이 작성한 코드에서도 유용한 예외는 동일한 가치가 있습니다. 이렇게 하면 코드를 사용할 사용자(무엇보다도 자기자신!)가 에러가 발생된 원인을 쉽게 찾을 수 있습니다.

자기자신의 예외를 발생시키는 방법은 raise 문을 사용하는 것입니다. 예를 들어:

raise RuntimeError("my error message")
---------------------------------------------------------------------------

RuntimeError Traceback (most recent call last)

<ipython-input-14-b1834d213d3b> in <module>()
----> 1 raise RuntimeError("my error message")

RuntimeError: my error message

이에 대한 유용한 예를 위해서 앞서 정의한 fibonacci 함수로 돌아가 보죠:

def fibonacci(N):
    L = []
    a, b = 0, 1
    while len(L) < N:
        a, b = b, a + b
        L.append(a)
    return L

이 함수에서 한 가지 문제점은 입력값이 음수일 수 있다는 것입니다. 음수가 입력되어도 이 함수에서 어떤 에러도 발생시키지는 않습니다. 하지만 음수 N을 지원하지 않는다는 점을 사용자에게 알리고 싶습니다. 관례적으로 잘못된 파라미터 값에의해 발생한 에러는 ValueError를 발생시킵니다:

def fibonacci(N):
    if N < 0:
        raise ValueError("N은 음수가 아니어야 합니다")
    L = []
    a, b = 0, 1
    while len(L) < N:
        a, b = b, a + b
        L.append(a)
    return L
fibonacci(10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fibonacci(-10)
---------------------------------------------------------------------------

ValueError Traceback (most recent call last)

<ipython-input-17-f1ae0a8066f0> in <module>()
----> 1 fibonacci(-10)

<ipython-input-15-2733925b15d0> in fibonacci(N)
1 def fibonacci(N):
2 if N < 0:
----> 3 raise ValueError("N은 음수가 아니어야 합니다")
4 L = []
5 a, b = 0, 1

ValueError: N은 음수가 아니어야 합니다

이제 사용자는 왜 입력이 잘못되었는지 정확히 알게됩니다. 심지어 tryexcept 블럭을 사용하여 이를 캐치할 수 있습니다!

N = -10
try:
    print("피보나치를 시도..")
    print(fibonacci(N))
except ValueError:
    print("잘못된 값: 무언가 다른 조치가 필요하다")
피보나치를 시도..
잘못된 값: 무언가 다른 조치가 필요하다

예외에 대해 자세히 알아보기

간단히 앞으로 볼 수 있는 다른 개념에 대해 언급하겠습니다. 너무 자세한 개념이나 왜, 어떻게 사용하는지 설명하지 않겠습니다. 대신 간단한 사용법을 보이고 나중에 스스로 더 찾아보도록 돕겠습니다.

에러 메세지 참조

tryexcept 문에서 이따금 에러 메세지 자체를 다루어야할 때가 있습니다.
이럴 때는 as 키워드를 사용합니다:

try:
    x = 1 / 0
except ZeroDivisionError as err:
    print("에러 클래스: ", type(err))
    print("에러 메세지:", err)
에러 클래스: <class 'ZeroDivisionError'>
에러 메세지: division by zero

이 패턴을 사용해 함수의 예외 처리를 다양하게 커스터마이징할 수 있습니다.

독자적인 예외 정의하기

기본 예외말고도 클래스 상속을 통해 독자적인 예외를 정의할 수 있습니다. 예를 들어 특별한 종류의 ValueError가 필요하면 다음과 같이 씁니다:

class MySpecialError(ValueError):
    pass

raise MySpecialError("here's the message")
---------------------------------------------------------------------------

MySpecialError Traceback (most recent call last)

<ipython-input-20-1c1bb7b055e0> in <module>()
2 pass
3
----> 4 raise MySpecialError("here's the message")

MySpecialError: here's the message

이 에러만 캐치하는 tryexcept 블럭을 사용할 수 있습니다:

try:
    print("작업 수행")
    raise MySpecialError("[상세한 에러 정보]")
except MySpecialError:
    print("다른 작업 수행")
작업 수행
다른 작업 수행

커스터마이징이 많이 필요한 코드일수록 이 용법이 유용합니다.

try…except…else…finally

tryexcept 외에 elsefinally 키워드를 사용해 예외 처리를 정교하게 튜닝할 수 있습니다. 기본적인 구조는 다음과 같습니다:

try:
    print("어떤 작업을 시도합니다")
except:
    print("실패할 경우 실행됩니다")
else:
    print("성공할 경우 실행됩니다")
finally:
    print("무조건 실행됩니다")
어떤 작업을 시도합니다
성공할 경우 실행됩니다
무조건 실행됩니다

else의 용도는 명확하지만 finally는 왜 필요할까요? 글쎄요, finally 절은 실제로 에러가 있던 없던 실행됩니다. 보통 어떤 작업이 완료되고 나서 자원을 정리하는 경우에 사용되곤 합니다.

 

< 함수 | 목차 | 반복자 >

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.