끝나지 않는 프로그래밍 일기

1. 예외 처리(Exception Handling)


오늘은 예외 처리(Exception Handling)에 대해 알아보려고 합니다. 여기서 '예외(Exception)'란 어떤 것일까요? 우리가 프로그램을 사용하다 보면 예기치 못한 상황으로 에러가 발생하여 비정상 종료되는 경험을 해보신적이 있으신가요? 예를 들어, 프로그램 내에서 존재하지 않는 파일을 열려고 한다던가 피제수를 0으로 나누려고 하는 등 런타임 도중 발생하는 에러를 예외라고 하며, 이는 프로그램의 작업 수행을 막아버리는 존재입니다. 우선 예외가 발생하는 상황을 한번 보도록 하겠습니다.

>>> 2013 * (1229/0)
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    2013 * (1229/0)
ZeroDivisionError: division by zero
>>> open('notfind.txt', 'r')
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    open('notfind.txt', 'r')
FileNotFoundError: [Errno 2] No such file or directory: 'notfind.txt'
>>> lst = [1, 2, 3, 4]
>>> lst[5]
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    lst[5]
IndexError: list index out of range

위를 보시면, 피제수를 0으로 나누려고 하니 ZeroDivisionError란 예외가 발생하고, 존재하지 않는 파일을 열려고 하니 FileNotFoundError란 예외가 발생하며, 위치(index)가 범위를 벗어나면 IndexError란 예외가 발생합니다. 이 밖에도 예외는 여러가지가 있으며 파이썬에서는 이런 예외를 처리하여 실행 도중에 에러가 발생해도 예외를 무시하거나 따로 처리할 수 있도록 try와 except란 녀석을 지원합니다. 한번 보도록 할까요?


2. try~except


파이썬에서 예외를 처리하기 위해서는 try~except절을 사용할 수 있는데, 아래는 이 절의 기본적인 형태입니다.

try:
   예외를 유발할 수 있는 구문
except <예외 종류>:
   예외 처리를 수행하는 구문

위의 형태를 한번 봐보시면, try절의 영역에는 예외가 발생할 수 있는 구문이 들어가며 except절의 영역에서는 try절에서 예외가 발생하였을때 잡아서 처리를 수행하는 구문이 들어갑니다. 우선은 먼저 try~except절이 사용된 예제를 보고 설명해드리도록 하겠습니다. (예외 구조도를 참고하시려면 여기를 클릭하세요)

>>> try:
	a = 10 / 0
except ZeroDivisionError:
	print('제수는 0이 될 수 없습니다!')

제수는 0이 될 수 없습니다!

위의 예제를 보시면, try절의 영역 내에서 ZeroDivisionError 예외가 발생할 때 그 예외를 잡아 처리하는 문장입니다. 피제수를 제수로 나눌 때, 제수가 0일 경우 '제수는 0이 될 수 없습니다!'라는 문장을 출력합니다. 이번에는 다른 예제를 보도록 하겠습니다.

>>> try:
	a = int(input("첫번째 숫자를 입력하세요: "))
	b = int(input("두번째 숫자를 입력하세요: "))
	print("a + b = ", a + b)
except ValueError:
	print('값이 적절하지 않습니다.')

첫번째 숫자를 입력하세요: 50
두번째 숫자를 입력하세요: 이십
값이 적절하지 않습니다.

위 예제에서는 input 내장 함수로 입력받은 값이 적절하지 않은 값이라면 ValueError 예외가 발생하여 '값이 적절하지 않습니다.'라는 문장을 출력시킵니다. 위에서는 a에 50이란 정상적인 값이 들어갔지만, b에는 '이십'이란 문자열을 입력받아 이를 정수로 변환시키려 하니 에러가 발생한 것입니다. 이제는 위 예제들을 적절히 섞어서 여러 개의 예외를 처리하는 예제를 한번 보도록 하겠습니다.

>>> try:
	a = int(input("피제수를 입력하세요: "))
	b = int(input("제수를 입력하세요: "))
	print("a / b = ", a / b)
except ValueError:
	print('값이 적절하지 않습니다.')
except ZeroDivisionError:
	print('제수는 0이 될 수 없습니다!')
	
피제수를 입력하세요: 10
제수를 입력하세요: 0
제수는 0이 될 수 없습니다!

위 예제를 보시면 except절은 한번만 사용될 수 있는게 아니라, 여러번 등장이 가능합니다. 위와 같이 예외 처리 영역을 나눌 수도 있지만, 아래와 같이 예외 처리 영역을 합칠 수도 있습니다.

>>> try:
	a = int(input("피제수를 입력하세요: "))
	b = int(input("제수를 입력하세요: "))
	print("a / b = ", a / b)
except (ValueError, ZeroDivisionError):
	print('제수가 0이거나 값이 적절하지 않습니다.')

피제수를 입력하세요: 10
제수를 입력하세요: 영
제수가 0이거나 값이 적절하지 않습니다.

위 예제에서는 소괄호를 사용하여 튜플을 이용해 예외를 처리합니다. 만약 try절의 영역 내에서 ValueError가 발생해도 예외가 처리되며, ZeroDivisionError가 발생해도 except절의 예외 처리 영역으로 이동합니다. 이 말은 즉슨, 튜플 내에 명시된 에러를 모두 처리하겠다는 말이 됩니다. 한마디로 말하자면 에러를 묶어서 처리하는 방법입니다. 마지막으로, 예외 인스턴스 객체를 통한 예외를 처리하는 방법을 보도록 하겠습니다.

>>> try:
	a = 50 / "이십"
except TypeError as e:
	print('예외:', e.args[0])
	
예외: unsupported operand type(s) for /: 'int' and 'str'

위 예제를 보시면, 에러 메시지를 담은 예외 인스턴스 객체를 e로 받아서 사용하는데 이 변수를 통해서 추가적인 정보를 출력해낼 수 있습니다. 위에선 정수와 문자열 간의 나눗셈 연산은 지원하지 않는다는 에러 메시지가 출력됩니다. 여기까지 잘 따라오셨나요? 그럼 다음으로 넘어가겠습니다.


3. else


except절이 예외가 발생하면 처리하는 영역이라면, else절은 예외가 발생하지 않았을 경우에 작업이 수행되는 영역입니다. 이 else절은 모든 except 절의 가장 마지막에 등장하여야 하며, 필요에 의해 else절을 달 수도 있고 달지 않을 수도 있습니다. 한마디로 선택적으로 사용할 수 있다는 것입니다. 우선은 try~except~else절의 기본적인 형태를 보도록 하겠습니다.

try:
   예외를 유발할 수 있는 구문
except <예외 종류>:
   예외 처리를 수행하는 구문
else:
   예외가 발생하지 않을 경우 수행할 구문

위 try절의 영역에서 예외가 발생하면, except절의 예외 처리 영역으로 이동하지만 예외가 발생하지 않는다면 else절의 영역으로 이동합니다. 우선은 예제를 먼저 보도록 하겠습니다.

>>> try:
	f = open('test.txt', 'r')
except IOError:
	print('파일을 열지 못했습니다.')
else:
	print('test.txt:', f.read())
	f.close()

test.txt: 테스트 파일!

위 예제는 test.txt 파일을 읽기 모드로 여는데 I/O에 관련된 예외가 발생하면 '파일을 열지 못했습니다.'라는 문장이 출력되고, 예외가 발생하지 않았다면 그 파일의 내용을 출력하고 파일을 닫습니다. 간단하죠? 


4. finally


finally절은 예외가 발생하든 말든 실행되는 영역입니다. try 영역이 실행되고 나서, 예외 발생여부와 상관없이 무조건 수행되는 영역입니다. 이 finally절도 else절과 같이 선택적으로 사용할 수 있는 영역입니다. 우선은 try~except~finally절의 기본적인 형태를 보도록 하겠습니다.

try:
   예외를 유발할 수 있는 구문
except <예외 종류>:
   예외 처리를 수행하는 구문
finally:
   예외 발생 여부과 상관없이 항상 실행되는 구문

위 try절의 영역에서 예외가 발생하든 발생하지 않든 finally절 영역의 구문이 실행됩니다. 간단하게 한번 예제를 보도록 하겠습니다.

>>> try:
	a = 10 / 0
except ZeroDivisionError:
	print('제수는 0이 될 수 없습니다!')
finally:
	print('무조건 실행되는 영역!')

제수는 0이 될 수 없습니다!
무조건 실행되는 영역!

위 예제에서는 try절의 영역에서 ZeroDivisionError 예외가 발생하지만, 그것과는 관계없이 finally절의 영역으로 들어가는 것을 보실 수 있습니다. 직접 예제에서 제수가 0이 아닌 다른 수로 바꾸어 예외가 발생하지 않게 되더라도 finally절 영역으로 들어가서 '무조건 실행되는 영역!'이라는 문장을 출력합니다.


5. raise


예기치 못한 상황으로 예외가 발생하는 경우도 있지만, 의도적으로 개발자가 예외를 발생시켜야 할 경우도 있습니다. 이럴 경우 raise 구문을 통하여 해당하는 예외를 강제로 발생시킬 수 있습니다. raise 구문을 사용하는 방법은 아래와 같습니다.

raise [예외]
raise [예외(데이터)]

예를 들어서 'raise TypeError'는 TypeError 예외를 강제적으로 발생시키게 하는 것이고, 'raise NameError("데이터!")'는 TypeError 예외를 강제로 발생시키면서 '데이터!'를 예외의 인자로 지정하는 것입니다. 우선은 먼저 예제를 보고 raise문이 어떤 역할을 하는지 보도록 하겠습니다.

>>> raise NameError
Traceback (most recent call last):
  File "<pyshell#116>", line 1, in <module>
    raise NameError
NameError
>>> raise NameError('예외 발생!')
Traceback (most recent call last):
  File "<pyshell#117>", line 1, in <module>
    raise NameError('예외 발생!')
NameError: 예외 발생!

위 예제를 보시면, raise 구문을 통해 NameError 예외를 강제로 발생시키고 있습니다. 두번째 raise 구문은 예외의 인자를 지정해 준 것으로써, 지정한 인자와 함께 같이 출력이 되는 것을 확인하실 수 있습니다. 다른 예제를 한번 보시죠.

>>> try:
	a = int(input('피제수를 입력하세요: '))
	b = int(input('제수를 입력하세요: '))
	if a <= 0 or b <= 0:
		raise ArithmeticError('피제수 혹은 제수가 0 이하일 수 없습니다.')
except ArithmeticError as e:
	print('예외 발생:', e.args[0])

피제수를 입력하세요: 2030
제수를 입력하세요: -10
예외 발생: 피제수 혹은 제수가 0 이하일 수 없습니다.

위 예제에서는 피제수 혹은 제수가 0 이하일 경우 ArithmeticError 예외를 발생시키도록 하였습니다. 피제수는 0 초과니 조건 성립이 되지 않았지만, 제수가 -10로 0 이하의 수니 조건문이 실행되어 예외를 발생시킨 것입니다. 이해가 되시나요? 아직 이해가 되지 않으신것 같다면 위에 있는 예제들을 응용하여 코드를 여러번 작성하여 보도록 해보세요. 계속 경험과 노력이 쌓이다 보면 그에 따른 좋은 결과가 있으리라 믿습니다.