예외

예외(Exception)는 프로그램에서 예외적인(exceptional) 상황이 발생할 때 일어납니다. 예를 들어, 파일을 읽으려고 하는데 파일이 존재하지 않으면 어떻게 될까요? 또는 프로그램이 실행 중일 때 뜻하지 않게 프로그램을 삭제하면 어떻게 될까요? 이러한 상황을 예외를 이용해 처리합니다.

이와 비슷하게 만약 프로그램에 유효하지 않은 문장이 있다면 어떻게 될까요? 이는 파이썬에 의해 처리되는데, 파이썬이 손을 들어(raise) 오류(error)가 있다고 말해줍니다.

오류

간단한 print 함수 호출을 생각해 봅시다. print를 잘못 써서 Print라고 하면 어떻게 될까요? 대소문자를 눈여겨보세요. 이 경우 파이썬은 문법 오류(syntax error)를 발생시킵니다(raise).

>>> Print("Hello World")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'Print' is not defined
>>> print("Hello World")
Hello World

보다시피 NameErorr가 발생하고 이 오류가 발견된 위치도 출력됩니다. 이것이 바로 이 오류에 대해 오류 처리기(error handler)가 하는 일입니다.

예외

사용자가 입력한 내용을 읽으려고 시도해(try) 보겠습니다. 다음 코드의 첫 번째 줄을 입력한 다음 엔터 키를 누릅니다. 컴퓨터가 사용자의 입력을 기다리고 있을 때 엔터 대신 macOS에서는 [ctrl-d]를, 윈도우에서는 [ctrl-z]를 누르고 어떻게 되는지 봅시다. (윈도우를 사용 중이고, 앞에서 말한 방법이 통하지 않으면 명령 프롬프트에서 [ctrl-c]를 눌러 KeyboardInterrupt 오류를 대신 일으킬 수 있습니다).

>>> s = input('Enter something --> ')
Enter something --> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
EOFError

파이썬이 예상치 못한 결과를 보고 EOFError라는 오류를 일으키는데, 이 오류는 기본적으로 파일의 끝 기호(ctrl-d로 표현되는)를 발견했다는 것을 의미합니다.

예외 처리

예외는 try..except 문을 이용해 처리할 수 있습니다. 기본적으로 일반 문장을 try 블록에 넣고 모든 오류 처리기를 except 블록에 넣습니다.

예제(exceptions_handle.py):

try:
    text = input('Enter something --> ')
except EOFError:
    print('Why did you do an EOF on me?')
except KeyboardInterrupt:
    print('You cancelled the operation.')
else:
    print('You entered {}'.format(text))

출력 결과:

# Press ctrl + d
$ python exceptions_handle.py
Enter something --> Why did you do an EOF on me?

# Press ctrl + c
$ python exceptions_handle.py
Enter something --> ^CYou cancelled the operation.

$ python exceptions_handle.py
Enter something --> No exceptions
You entered No exceptions

동작 원리

예외/오류를 일으킬 수도 있는 모든 문장을 try 블록에 넣고 각 오류/예외에 대한 처리기를 except 절/블록에 넣습니다. except 절에서는 하나의 특화된 오류나 예외 또는 괄호로 감싼 오류/예외 리스트를 처리할 수 있습니다. 오류나 예외의 이름을 지정하지 않으면 모든 오류나 예외를 처리합니다.

try 절과 연관된 except 절이 적어도 하나는 있어야 한다는 점에 유의합니다. 그렇게 하지 않으면 try 블록을 만들 이유가 없을 것입니다.

아무런 오류나 예외도 처리되지 않으면 기본 파이썬 처리기가 호출되어 프로그램의 실행을 중단하고 오류 메시지를 출력합니다. 이를 이미 앞에서 실제로 살펴본 바 있습니다.

try..except 블록과 연관된 else 절을 둘 수도 있습니다. else 절은 아무런 예외가 발생하지 않을 경우 실행됩니다.

다음 예제에서는 추가 정보를 얻을 수 있도록 예외 객체를 가져오는 방법을 알아보겠습니다.

예외 발생시키기

raise 문에 오류/예외의 이름과 발생시킬(throw) 예외 객체를 지정해 예외를 발생시킬 수 있습니다.

발생시킬 수 있는 오류나 예외는 Exception 클래스에서 직접적으로 또는 간접적으로 파생된 클래스여야 합니다.

예제(exceptions_raise.py):

class ShortInputException(Exception):
    '''사용자 정의 예외 클래스'''
    def __init__(self, length, atleast):
        Exception.__init__(self)
        self.length = length
        self.atleast = atleast

try:
    text = input('Enter something --> ')
    if len(text) < 3:
        raise ShortInputException(len(text), 3)
    # 이곳에서 다른 작업을 계속할 수 있습니다
except EOFError:
    print('Why did you do an EOF on me?')
except ShortInputException as ex:
    print(('ShortInputException: The input was ' +
           '{0} long, expected at least {1}')
          .format(ex.length, ex.atleast))
else:
    print('No exception was raised.')

출력 결과:

$ python exceptions_raise.py
Enter something --> a
ShortInputException: The input was 1 long, expected at least 3

$ python exceptions_raise.py
Enter something --> abc
No exception was raised.

동작 원리

여기서는 예외 타입을 직접 만듭니다. 이 새로운 예외 타입의 이름은 ShortInputException입니다. 여기에는 필드가 두 개인데, length는 입력의 길이이고, atleast는 프로그램에서 기대하는 최소 길이를 나타냅니다.

except 절에서는 as로 각 오류/예외 클래스의 객체를 담을 변수명을 지정합니다. 이것은 함수를 호출할 때의 매개변수 및 인수에 비유할 수 있습니다. 이 같은 특정한 except 절 내에서는 예외 객체의 lengthatleast 필드를 이용해 적절한 메시지를 사용자에게 출력할 수 있습니다.

try ... finally

프로그램에서 파일을 읽고 있다고 가정해 봅시다. 이때 파일 객체가 적절히 닫혔거나 예외가 발생하지 않았는지 어떻게 확인할 수 있을까요? finally 블록을 이용하면 됩니다.

다음 프로그램을 exceptions_finally.py라는 이름으로 저장합니다.

import sys
import time

f = None
try:
    f = open("poem.txt")
    # 일반적인 파일 읽기 코드
    while True:
        line = f.readline()
        if len(line) == 0:
            break
        print(line, end='')
        sys.stdout.flush()
        print("Press ctrl+c now")
        # 잠시 동안 실행되게 합니다
        time.sleep(2)
except IOError:
    print("Could not find file poem.txt")
except KeyboardInterrupt:
    print("!! You cancelled the reading from the file.")
finally:
    if f:
        f.close()
    print("(Cleaning up: Closed the file)")

출력 결과:

$ python exceptions_finally.py
Programming is fun
Press ctrl+c now
^C!! You cancelled the reading from the file.
(Cleaning up: Closed the file)

동작 원리

예제에서는 일반적인 파일 읽기를 수행하다가 각 줄을 출력한 후 time.sleep 함수를 이용해 임의로 2초 동안 잠들게 해서 프로그램이 느리게 동작하게 만듭니다(파이썬은 기본적으로 매우 빠르게 동작합니다). 프로그램이 동작하는 도중에 Ctrl + C를 눌러 프로그램을 중단 또는 취소시킵니다.

그러면 KeyboardInterrupt 예외가 발생하고 프로그램이 중단됩니다. 하지만 프로그램이 종료되기 전에 finally 절이 실행되어 언제나 파일 객체가 닫힙니다.

참고로 값이 0이나 None 값이 할당되거나 빈 시퀀스나 빈 컬렉션은 파이썬에서 False로 간주됩니다. 앞의 코드에서 if f:를 쓸 수 있는 것은 바로 이런 이유 때문입니다.

또한 print 다음에 sys.stdout.flush()를 사용해 화면에 곧바로 출력하게 했다는 점도 유의합니다.

with 문

try 블록에서 리소스(resource)를 획득하고 나중에 finally 블록에서 리소스를 해제하는 것은 일반적인 패턴입니다. 그래서 이를 깔끔하게 처리할 수 있는 with 문도 있습니다.

다음 프로그램을 exceptions_using_with.py라는 이름으로 저장합니다.

with open("poem.txt") as f:
    for line in f:
        print(line, end='')

동작 원리

출력 결과는 앞의 예제와 같을 것입니다. 차이점은 open 함수를 with 문과 함께 사용해 with open에 의해 파일 닫기가 자동으로 이뤄진다는 것입니다.

이때 내부적으로 with 문에 의한 프로토콜이 동작합니다. with 문은 open 문에서 반환하는 객체를 가져오는데, 이를 "thefile"이라고 부르기로 합시다.

with 문에서는 코드 블록이 시작하기 전에 항상 thefile.__enter__를 호출하고, 코드 블록이 끝나면 항상 thefile.__exit__를 호출합니다.

그래서 finally 블록에 작성해야 할 법한 코드는 자동으로 __exit__ 메서드에 의해 처리될 것입니다. 이렇게 하면 굳이 try..finally 문을 반복적으로 작성하지 않아도 됩니다.

이 주제에 대한 자세한 내용은 이 책의 범위를 벗어나므로 좀 더 포괄적인 설명에 대해서는 PEP 343을 참고하기 바랍니다.

정리

이번 장에서는 try..excepttry..finally 문의 사용법을 알아봤습니다. 직접 예외 타입을 만들고 예외를 어떻게 발생시키는지도 살펴봤습니다.

다음 장에서는 파이썬 표준 라이브러리를 살펴보겠습니다.