프로그래밍 기본 요소: 오류 및 예외 처리

아래 코드를 실행하면 숫자를 입력하라는 창이 나오며, 여기에 숫자 3을 입력하면 정상적으로 작동한다. 하지만, 예를 들어, 3.2를 입력하면 값 오류(value error)가 발생한다.

In [1]:
input_number = input("A number please: ")
number = int(input_number)

print("제곱의 결과는", number**2, "입니다.")
A number please: 3
제곱의 결과는 9 입니다.
In [2]:
input_number = input("A number please: ")
number = int(input_number)

print("제곱의 결과는", number**2, "입니다.")
A number please: 3.2
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-3adc5931e3f0> in <module>
      1 input_number = input("A number please: ")
----> 2 number = int(input_number)
      3 
      4 print("제곱의 결과는", number**2, "입니다.")

ValueError: invalid literal for int() with base 10: '3.2'

위 오류가 발생하는 이유는 int 함수가 정수 모양이 아닌 문자열 인자와 함께 실행되었기 때문이다.

이와 같이 프로그램을 작성하거나 실행하다보면 다양한 오류가 발생할 수 있다. 다양한 예제를 통해 발생 오류 상황, 오류 정보 확인 방법, 오류를 다루는 방법 등을 소개한다.

오류의 종류

앞서 예제들을 통해 살펴 보았듯이 다양한 종류의 오류가 발생하며, 코드가 길어지거나 복잡해지면 오류가 발생할 가능성은 점차 커진다. 오류의 종류를 파악하면 어디서 왜 오류가 발생하였는지를 보다 쉽게 파악하여 코드를 수정할 수 있게 된다. 따라서 코드의 발생원인을 바로 알아낼 수 있어야 하며 이를 위해서는 오류 메시지를 제대로 확인할 수 있어야 한다. 하지만 여기서는 언급된 예제 정도의 수준만 다루고 넘어간다.

코딩을 하다 보면 어차피 다양한 오류와 마주치게 될 텐데 그때마다 스스로 오류의 내용과 원인을 확인해 나가는 과정을 통해 보다 많은 경험을 쌓는 길 외에는 달리 방법이 없다.

가장 많이 발생하는 오류들을 살펴보자.

ZeroDivisionError: 0으로 나누기 오류

나눗셈을 숫자 0으로 시도할 때 발생하는 오류이다.

In [3]:
divByZero = 4.6/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-3-f4d1cd99e916> in <module>
----> 1 divByZero = 4.6/0

ZeroDivisionError: float division by zero

SyntaxError: 구문 오류

문법에 맞지 않는 표현식 또는 명령문을 사용하는 경우 발생하는 오류이다.

예를 들어, 문자열은 작은 따옴표 또는 큰 따옴표로 감싸야 하는데, 제대로 하지 않으면 아래와 같은 오류가 발생한다.

In [4]:
lovePython = '파이썬이 좋아요!
  File "<ipython-input-4-785974a1ae1e>", line 1
    lovePython = '파이썬이 좋아요!
                           ^
SyntaxError: EOL while scanning string literal

감싸주는 따옴표가 달라도 구문 오류가 발생한다.

In [5]:
lovePython = '파이썬이 좋아요!"
  File "<ipython-input-5-84e254a17e87>", line 1
    lovePython = '파이썬이 좋아요!"
                            ^
SyntaxError: EOL while scanning string literal

IndentationError: 들여쓰기 오류

들여쓰기를 하지 않거나 들여쓰기의 정도가 다를 때 발생하는 오류이다. 예를 들어, for 반복문의 본체 명령문에 사용된 들여쓰기 일정하지 않으면 아래와 같은 오류가 발생한다.

In [6]:
for i in range(3):
    j = i * 2  
      print(i, j)              # 잘못된 들여쓰기
  File "<ipython-input-6-13e8ac870705>", line 3
    print(i, j)              # 잘못된 들여쓰기
    ^
IndentationError: unexpected indent

TypeError: 자료형 오류

함수의 인자로 사용되는 값이 지원되지 않는 자료형일 때 발생한다. 예를 들어, 문자열의 덧셈, 뺄셈 등은 지원되지 않는다.

In [7]:
'cat' - 'dog'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-47e5dc8c518e> in <module>
----> 1 'cat' - 'dog'

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

NameError: 이름 오류

정의되어 있지 함수 또는 선언되지 않은 변수를 사용할 때 발생한다. 예를 들어, 아래에서 data는 전혀 알려지지 않아서 오류를 발생시킨다.

In [8]:
print(data)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-8-dbd883db58b7> in <module>
----> 1 print(data)

NameError: name 'data' is not defined

IndexError: 인덱스 오류

문자열, 리스트, 튜플 등 서열(sequence) 자료형은 항목들의 순서를 인덱스를 이용하여 정한다. 그런데 인덱싱, 슬라이싱 등에 사용되는 인덱스가 지정된 범위를 벗어나면 오류가 발생한다.

In [9]:
print('I love to program using Python.'[100])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-54231f4a6af3> in <module>
----> 1 print('I love to program using Python.'[100])

IndexError: string index out of range

ValueError: 값 오류

함수의 인자로 사용되는 값이 자료형은 맞지만 처리할 수 없는 경우 발생한다.

예를 들어, int 함수의 인자로 부동소수점 모양의 문자여을을 사용하면 오류가 발생한다.

In [10]:
int('3.2')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-10-24a9bf77383c> in <module>
----> 1 int('3.2')

ValueError: invalid literal for int() with base 10: '3.2'

AttributeError: 속성 오류

지원되지 않는 메서드를 사용하려 할 경우 발생한다. 예를 들어, 문자열에는 len 메서드가 지원되지 않는다.

In [11]:
'HelloPython'.len()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-11-f23430ad9980> in <module>
----> 1 'HelloPython'.len()

AttributeError: 'str' object has no attribute 'len'

오류 확인 방법

앞서 보았듯이 오류가 발생할 때마다 파이썬 해석기가 발생한 오류의 종류와 발생위치에 대한 정보를 전달한다. 발생위치는 line을 이용하고, 오류의 종류는 맨 마지막줄 첫 부분에 명시한다.

예제

In [12]:
sentence = 'I am a sentence
  File "<ipython-input-12-6139954f30c6>", line 1
    sentence = 'I am a sentence
                               ^
SyntaxError: EOL while scanning string literal

오류를 확인하는 메시지가 처음 볼 때는 매우 생소하다. 위 오류 메시지를 간단하게 살펴보면 다음과 같다.

  • File "<ipython-input-23-...", line 1: 1번 줄에서 오류 발생
  • sentence = 'I am a sentence: 오류 발생 위치 명시

  • SyntaxError: EOL while scanning string literal: 오류 종류 표시: 구문 오류(SyntaxError)

예제

아래 예제는 0으로 나눌 때 발생하는 오류를 나타낸다. 오류에 대한 정보를 잘 살펴보면서 어떤 내용을 담고 있는지 확인해 보아야 한다.

In [13]:
a = 0
4/a
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-13-114f99fa90cb> in <module>
      1 a = 0
----> 2 4/a

ZeroDivisionError: division by zero

예외 처리

코드에 문법 오류가 포함되어 있는 경우 아예 실행되지 않는다. 그렇지 않은 경우에는 일단 실행이 되고 중간에 오류가 발생하면 바로 멈춰버린다.

이렇게 중간에 오류가 발생할 수 있는 경우를 미리 생각하여 대비하는 과정을 예외 처리(exception handling)라고 부른다.

예를 들어, 오류가 발생하더라도 오류발생 이전까지 생성된 정보들을 저장하거나, 오류발생 이유를 좀 더 자세히 다루거나, 아니면 오류발생에 대한 보다 자세한 정보를 사용자에게 알려주기 위해 예외 처리를 사용한다.

사용방식은 다음과 같다.

try:
    코드1
except:
    코드2
  • 먼저 코드1 부분을 실행한다.
  • 코드1 부분이 실행되면서 오류가 발생하지 않으면 코드2 부분은 무시하고 다음으로 넘어간다.
  • 코드1 부분이 실행되면서 오류가 발생하면 더이상 진행하지 않고 바로 코드2 부분을 실행한다.

예제

아래 코드를 실행할 때 int() 함수가 정수 모양의 문자열만 처리할 수 있기 때문에 예를 들어, 3.2를 입력하면 오류가 발생한다. 이런 경우를 대비하기 위해 예외처리를 사용해야 한다.

In [15]:
number_to_square = input("A number please:")

try: 
    number = int(number_to_square)
    print("제곱의 결과는", number ** 2, "입니다.")
except:
    print("정수를 입력해야 합니다.")
A number please:3.2
정수를 입력해야 합니다.

오류 종류에 맞추어 다양한 대처를 하기 위해서는 오류의 종류를 명시하여 예외 처리를 하면 된다. 아래 코드는 입력 갑에 따라 다른 오류가 발생하고 그에 상응하는 방식으로 예외 처리를 실행한다.

예제: ValueError(값 오류) 예외 처리

3.2를 입력하면 ValueError가 발생한다.

In [16]:
number_to_square = input("A number please: ")

try: 
    number = int(number_to_square)
    a = 5/(number - 4)
    print("결과는", a, "입니다.")
except ValueError:
    print("정수를 입력해야 합니다.")
except ZeroDivisionError:
    print("4는 빼고 하세요.")
A number please: 3.2
정수를 입력해야 합니다.

예제: ZeroDivisionError(0으로 나누기 오류) 예외 처리

4를 입력하면 ZeroDivisionError 가 발생한다.

In [19]:
number_to_square = input("A number please: ")

try: 
    number = int(number_to_square)
    a = 5/(number - 4)
    print("결과는", a, "입니다.")
except ValueError:
    print("정수를 입력해야 합니다.")
except ZeroDivisionError:
    print("4 이외의 값을 입력하세요.")
A number please: 4
4 이외의 값을 입력하세요.

이와 같이 발생할 수 예외를 가능한 한 모두 염두하는 프로그램을 구현해야 하는 일은 매우 어려운 일이다. 하지만 오류의 종류를 틀리게 명시하면 예외 처리가 제대로 작동하지 않는다.

In [20]:
try:
    a = 1/0
except ValueError:
    print("This program stops here.")
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-20-0c0d6aaa1361> in <module>
      1 try:
----> 2     a = 1/0
      3 except ValueError:
      4     print("This program stops here.")

ZeroDivisionError: division by zero

raise 함수

강제로 오류를 발생시키고자 하는 경우에 사용한다. 예를 들어, 어떤 함수를 정확히 정의하지 않은 상태에서 다른 중요한 일을 먼저 처리하고자 할 때 아래와 같이 함수를 선언하고 넘어갈 수 있다.

그런데 아래 함수를 제대로 선언하지 않은 채로 다른 곳에서 호출하면

"아직 정의되어 있지 않음"

이란 메시지로 정보를 알려주게 된다.

In [21]:
def to_define():
    """아주 복잡하지만 지금 당장 불필요"""
    raise NotImplementedError("아직 정의되어 있지 않음")
In [22]:
print(to_define())
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-22-9ce224c9e0b6> in <module>
----> 1 print(to_define())

<ipython-input-21-57624ffe08b9> in to_define()
      1 def to_define():
      2     """아주 복잡하지만 지금 당장 불필요"""
----> 3     raise NotImplementedError("아직 정의되어 있지 않음")

NotImplementedError: 아직 정의되어 있지 않음

주의: 오류 처리를 사용하지 않으면 오류 메시지가 보이지 않을 수도 있음에 주의해야 한다.

In [23]:
def to_define1():
    """아주 복잡하지만 지금 당장 불필요"""
In [24]:
print(to_define1())
None

코드의 건전성 문제

문법 오류 또는 실행 중에 오류가 발생하지 않는다 하더라도 코드의 건전성(soundness)이 보장되지는 않는다. 코드의 건전성이라 함은 코드를 실행할 때 기대하는 결과가 산출된다는 것을 보장한다는 의미이다.

예를 들어, 아래 코드는 숫자의 제곱을 리턴하는 square() 함수를 제대로 구현하지 못한 경우를 다룬다.

In [25]:
def square(number):
    """
    정수를 인자로 입력 받아 제곱을 리턴한다.
    """
    
    square_of_number = number * 2
    
    return square_of_number

위 함수를 아래와 같이 호출하면 오류가 전혀 발생하지 않지만, 엉뚱한 값을 리턴한다.

In [26]:
square(3)
Out[26]:
6

help() 를 이용하여 어떤 함수가 무슨 일을 하는지 내용을 확인할 수 있다. 단, 함수를 정의할 때 함께 적힌 문서화 문자열(docstring) 내용이 확인된다. 따라서, 함수를 정의할 때 문서화 문자열에 가능한 유효한 정보를 입력해 두어야 한다.

In [29]:
help(square)
Help on function square in module __main__:

square(number)
    정수를 인자로 입력 받아 제곱을 리턴한다.

오류에 대한 보다 자세한 정보

파이썬에서 다루는 오류에 대한 보다 자세한 정보는 아래 사이트들에 상세하게 안내되어 있다.

예제

아래 코드는 100을 입력한 값으로 나누는 함수이다. 다만 0을 입력할 경우 0으로 나누기 오류(ZeroDivisionError)가 발생한다.

In [28]:
number_to_square = input("A number to divide 100: ")

number = int(number_to_square)
print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")
A number to divide 100: 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-28-16ca079711a9> in <module>
      2 
      3 number = int(number_to_square)
----> 4 print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")

ZeroDivisionError: division by zero

아래 내용이 충족되도록 위 코드를 수정하라.

  • 나눗셈이 부동소수점으로 계산되도록 한다.
  • 0이 아닌 숫자가 입력될 경우 100을 그 숫자로 나눈다.
  • 0이 입력될 경우 0이 아닌 숫자를 입력하라고 전달한다.
  • 숫자가 아닌 값이 입력될 경우 숫자를 입력하라고 전달한다.

견본답안:

In [30]:
number_to_square = input("A number to divide 100: ")

try: 
    number = float(number_to_square)
    print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")
except ZeroDivisionError:
    raise ZeroDivisionError('0이 아닌 숫자를 입력하세요.')
except ValueError:
    raise ValueError('숫자를 입력하세요.')    
A number to divide 100: 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-30-073c95bfe0cb> in <module>
      4     number = float(number_to_square)
----> 5     print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")
      6 except ZeroDivisionError:

ZeroDivisionError: float division by zero

During handling of the above exception, another exception occurred:

ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-30-073c95bfe0cb> in <module>
      5     print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")
      6 except ZeroDivisionError:
----> 7     raise ZeroDivisionError('0이 아닌 숫자를 입력하세요.')
      8 except ValueError:
      9     raise ValueError('숫자를 입력하세요.')

ZeroDivisionError: 0이 아닌 숫자를 입력하세요.
In [31]:
number_to_square = input("A number to divide 100: ")

try: 
    number = float(number_to_square)
    print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")
except ZeroDivisionError:
    raise ZeroDivisionError('0이 아닌 숫자를 입력하세요.')
except ValueError:
    raise ValueError('숫자를 입력하세요.')    
A number to divide 100: abc
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-31-073c95bfe0cb> in <module>
      3 try:
----> 4     number = float(number_to_square)
      5     print("100을 입력한 값으로 나눈 결과는", 100/number, "입니다.")

ValueError: could not convert string to float: 'abc'

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-31-073c95bfe0cb> in <module>
      7     raise ZeroDivisionError('0이 아닌 숫자를 입력하세요.')
      8 except ValueError:
----> 9     raise ValueError('숫자를 입력하세요.')

ValueError: 숫자를 입력하세요.