Python 기초

예외 처리

우리가 사는 세상은 완벽하지 않습니다. 사용자들은 잘못된 데이터를 입력할 수도 있고, 우리가 오픈하고자 하는 파일이 컴퓨터에 존재하지 않을 수도 있으며 인터넷이 다운될 수도 있습니다.

또 프로그래머에 의하여 많은 버그들이 프로그램에 추가됩니다. 예를 들어 리스트의 인덱스가 한계를 넘을 수도 있습니다.

지금까지는 이러한 문제들을 전혀 생각하지 않았지만 이번 장부터는 현실을 직시해봅시다.

 

오류가 발생하였다면 우리는 무엇을 어떻게 하여야 할까요? 먼저 침착하게 오류의 내용을 살펴보아야 합니다.

파이썬은 상당히 발전된 오류 보고 시스템을 가지고 있어 소스 파일의 몇 번째 문장에서 오류가 발생하였는지를 우리에게 알려줍니다.

따라서 해당 문장으로 가서 살펴보아야 할 것입니다.

 

예를 들어 파일을 열어 데이터를 읽는 프로그램에서 파일이 없다면 당장 오류가 발생되며 종료될 것입니다. 또 정수를 0으로 나눈다면 오류가 발생할 것입니다.

(x, y) = (2, 0)
z = x / y

 

<실행 결과>

ZeroDivisionError: division by zero

 

위의 프로그램에서 ZeroDivisionError 오류가 발생하였고 파이썬 인터프리터는 어디서 오류가 발생되었는지를 자세하게 보고합니다.

이러한 오류 메시지를 역추적(traceback) 메시지라고 합니다.

 

예외 처리

파이썬에서 실행 도중에 발생하는 오류를 예외(exception)라고 부릅니다. 만약 우리가 만든 프로그램을 사용하던 사용자가 오류를 만났다고 가정합시다.

대개의 경우 오류가 발생하면 프로그램이 종료됩니다. 오류가 발생하여서 사용자가 이제까지 작업하던 데이터를 모두 잃어버렸다면 사용자는 절망하게 될 것입니다.

따라서 우리는 오류가 발생했을 때 오류를 사용자에게 알려주고 모든 데이터를 저장하게 한 후에 사용자가 우아하게(gracefully) 프로그램을 종료할 수 있도록 하는 것이 바람직합니다.

또 오류를 처리한 후에 계속 실행할 수 있다면 더 나은 프로그램이 될 수 있습니다. 파이썬에서는 예외 처리를 통하여 이러한 기능을 제공할 수 있습니다.

 

NOTE

여기서 한 가지 주의할 점은 버그와 예외는 구별하여야 합니다. 실행 도중에 버그로 인해서도 실행 오류가 발생할 수 있지만 이러한 버그는 개발 과정에서 모두 수정되어야 합니다.

파이썬에서는 버그에 의한 실행 오류도 에외로 취급하지만 진정한 의미에서의 예외는 우리가 예상하였던 상황이 아닌 경우를 의미합니다.

예를 들면 반드시 존재하여야 하는 파일이 없거나 인터넷 서버가 다운된 경우 등을 진정한 의미에서의 예외라고 할 수 있습니다.

 

오류의 종류

프로그램에서 나타날 수 있는 문제들은 어떤 것이 있을까요? 일반적으로 아래와 같은 문제들을 생각할 수 있습니다.

  • 사용자 입력 오류 : 사용자가 정수를 입력하여야 하는데 실수를 입력할 수 있습니다.
  • 장치 오류 : 네트워크가 안 된다거나 하드 디스크 작동이 실패할 수 있습니다.
  • 코드 오류 : 잘못된 인텍스를 사용하여서 리스트에 접근할 수 있습니다.

 

잘 알려진 오류에는 다음과 같은 것들이 있습니다.

  • IOError : 파일을 열 수 없으면 발생합니다.
  • ImportError : 파이썬이 모듈을 찾을 수 없으면 발생합니다.
  • ValueError : 연산이나 내장 함수에서 인수가 적절치 않은 값을 가지고 있으면 발생합니다.
  • KeyboardInterrupt : 사용자가 인터럽트 키를 누르면 발생합니다. (Control-C 나 Delete)
  • EOFError : 내장 함수가 파일의 끝을 만나면 발생합니다.

 

오류를 처리하는 전통적인 방법은 메소드가 오류 코드를 반환하는 것이지만 이 방법은 항상 가능한 것은 아닙니다. 그리고 상당히 코드가 지저분하게 됩니다.

파이썬에서는 try-catch 블록을 사용하여 오류를 질서정연하게 처리할 수 있습니다.

오류가 발생하면 프로그램의 정상적인 실행 흐름이 중단되고 오류를 설명하는 예외(exception)가 생성되며 이 예외가 오류 처리 코드로 전달됩니다.

 

그렇다면 파이썬에서 예외 처리기는 어떻게 작성하여야 할까요? 예외 처리기는 try 블록과 except 블록으로 이루어집니다.

기본적으로 try 블록에서 발생된 예외를 except 블록에서 처리합니다.

 

예외 처리기의 기본 형식은 아래와 같습니다.

try:
    예외가 발생할 수 있는 문장
except 오류 내용:
    예외를 처리하는 문장

 

먼저 try 블록에는 예외가 발생할 가능성이 있는 문장이 들어갑니다. except 블록에는 자신이 처리할 수 있는 예외의 종류를 지정하고 그 예외를 처리하기 위한 코드가 들어갑니다.

예를 들어서 앞에서 등장한 0으로 나누는 예외를 처리하려면 아래와 같이 코드를 작성하면 됩니다.

(x, y) = (2, 0)
try:
    z = x / y
except ZeroDivisionError:
    print("0으로 나누는 예외")

 

<실행 결과>

0으로 나누는 예외

 

만약 시스템이 내보내는 예외 메시지를 출력하고 싶으면 아래와 같이 합니다.

(x, y) = (2, 0)
try:
    z = x / y
except ZeroDivisionError as e:
    print(e)

 

<실행 결과>

division by zero

 

사용자가 숫자를 입력할 때도 오류가 발생할 수 있습니다. 예를 들어서 정수를 받아야 되는데 사용자가 실수를 입력하면 아래와 같이 오류가 발생합니다.

n = int(input("숫자를 입력하세요 : "))

 

<실행 결과>

  • 실수 23.5를 입력했을 때
ValueError: invalid literal for int() with base 10: '23.5'

 

이러한 경우에도 아래과 같이 try-except 구조를 사용하여 오류를 처리할 수 있습니다.

while True:
    try:
        n = input("숫자를 입력하세요 : ")
        n = int(n)
        break
    except ValueError:
        print("정수가 아닙니다. 다시 입력하세요.")
print("정수 입력이 성공하였습니다!")

 

<실행 결과>

숫자를 입력하세요 : 23.5
정수가 아닙니다. 다시 입력하세요.
숫자를 입력하세요 : 10
정수 입력이 성공하였습니다!

 

파일을 열 때도 오류가 많이 발생합니다. 파일 오류를 처리하는 문장을 작성해보면 아래와 같습니다.

try:
    fname = input("파일 이름을 입력하세요 : ")
    infile = open(fname, "r")
except IOError:
    print("파일 " + fname + "을 발견할 수 없습니다.")

 

<실행 결과>

파일 이름을 입력하세요 : bongmin.py
파일 bongmin.py을 발견할 수 없습니다.

 

try/except 블록에서의 실행 흐름

try/except 블록에서 예외가 발생하는 경우와 발생하지 않는 경우의 실행 흐름을 비교하여 봅시다.

먼저 예외가 발생하지 않는 경우에는 except 블록의 코드는 실행되지 않습니다. 반면에 예외가 발생한 경우에는 except 블록의 코드가 실행됩니다.

 

다중 예외 처리 구조

try:
    예외가 발생할 수 있는 문장
exception Exception1:
    Exception1이면 이 블록이 실행됩니다.
exception Exception2:
    Exception2이면 이 블록이 실행됩니다.
else:
    예외가 없는 경우에 실행됩니다.
  • 하나의 try 문장은 여러 개의 except 문장을 가질 수 있습니다. 이것은 하나의 문장에서 많은 예외가 발생할 수 있는 경우에 유용합니다.
  • 그냥 except만 적어주면 모든 예외를 처리하는 except 절이 됩니다.
  • except 절 뒤에는 else 절을 추가할 수 있습니다. 이것은 try 블록이 아무런 예외를 발생하지 않을 때 실행되는 블록입니다.
  • else 블록은 try 블록이 필요 없는 코드들을 넣어두는 곳입니다.

 

예를 들어봅시다. 파일을 열어서 문자열을 기록한 후에 발생할 수 있는 IOError 예외를 처리하고 예와가 발생하지 않았으면 성공적으로 파일에 기록하였다는 메시지를 출력하는 프로그램을 작성해봅시다.

try:
    fh = open("testfile", "w")
    fh.write("테스트 데이터를 파일에 씁니다.")
except IOError:
    print("Error : 파일을 찾을 수 없거나 데이터를 쓸 수 없습니다.")
else:
    print("파일에 성공적으로 기록하였습니다.")
    fh.close()

 

<실행 결과>

파일에 성공적으로 기록하였습니다.

 

만약 위의 예제에서 우리가 쓰기 권한을 가지고 있지 않은 파일에 기록하려고 했다면 아래와 같은 예외가 발생할 것입니다.

Error : 파일을 찾을 수 없거나 데이터를 쓸 수 없습니다.

 

예외가 기술되지 않은 except 블록

만약 우리가 except 블록을 만들 때 자세한 예외의 내용을 적지 않으면 어떠한 예외라도 처리할 수 있는 except 블록이 됩니다.

try:
    예외가 발생할 수 있는 문장
    .............................................
except:
    어떠한 예외라도 발생하면 여기서 처리합니다.
    .............................................
else:
    예외가 없다면 이 블록이 실행됩니다.

 

이러한 종류의 try-except 블록은 발생하는 어떤 예외라도 붙잡아서 처리할 수 있습니다.

하지만 이것은 좋은 방법은 아닌데 그 이유는 발생하는 모든 예외를 잡기 때문에 프로그래머가 문제의 근본 원인을 알 수 없기 때문입니다.

프로그래머가 구체적인 문제의 원인을 알 수 있도록 프로그램을 작성하여야 합니다.

 

여러 개의 예외를 가지는 except 블록

우리는 하나의 except 블록에서 여러 개의 예외를 처리할 수 있습니다. 아래와 같은 형식을 사용합니다.

try:
    예외가 발생할 수 있는 문장
    ............................................
except(Exception1[, Exception2[, .... ExceptionN]]):
    예외 리스트에 있는 예외가 발생하면 여기서 처리합니다.
    ............................................
else:
    예외가 없다면 이 블록이 실행됩니다.

 

try-finally 블록

finally 블록은 오류가 발생한 경우나 아니면 발생하지 않은 경우에도 항상 실행되어야 하는 문장을 두는 블록입니다. finally 블록은 항상 실행됩니다.

try:
    예외가 발생할 수 있는 문장
    ..........................................
    여기 문장은 예외가 발생하면 실행되지 않을 수 있습니다.
finally:
    항상 실행됩니다.
    ..........................................

 

파일을 다루는 경우에는 예외 여부와 상관없이 항상 파일을 닫아야 합니다. 이러한 경우에 finally 블록이 사용됩니다.

try:
    f = open("test.txt", "w")
    f.write("테스트 데이터를 파일에 씁니다!!!")
    # 파일 연산을 수행합니다.
except IOError:
    print("Error : 파일을 찾을 수 없거나 데이터를 쓸 수 없습니다.")
finally:
    f.close()

 

try 블록에서 예외가 발생하면 finally 블록이 먼저 즉시 실행됩니다. finally 블록의 모든 문장이 실행된 후에 예외가 다시 발생되고 except 블록에서 처리됩니다.