컴퓨터의 개념 및 실습
This project is maintained by jinsooya
Exception Handling
박 진 수 교수
Intelligent Data Semantics Lab
Seoul National University
문법 오류(syntax error)
논리 오류(logical error)
실행 오류(runtime error)
문법 오류는 컴파일 단계에서, 논리 오류와 실행 오류는 실행 단계에서 발생한다.
실행 오류가 발생하는 예를 살펴보자.
# 숫자를 0으로 나눠서 오류가 난다.
15 / 0
# 폴더에 없는 파일을 열려고 해서 오류가 난다.
f = open('hw1.py')
앞의 예처럼 예외 처리를 하지 않거나 예외 처리에 실패하면 프로그램을 실행하는 중 갑작스럽게 종료하게 된다. 예외가 발생하면 오류 메시지를 통해 예외가 발생한 줄의 번호를 확인할 수 있다. 뿐만 아니라 발생한 예외의 종류와 예외가 발생한 이유에 대한 간략한 설명을 화면으로 출력하기 때문에 그 원인을 확인할 수 있다. 따라서 프로그램 작성할 때 주의만 잘 기울이면 대부분의 예외 발생을 막을 수 있다. 예를 들어, 사용자가 입력한 값이 유효한 입력 값인지 확인하는 등의 예외 처리 코드만 미리 작성해 놓아도 많은 실행 오류를 방지할 수 있다.
다음 프로그램을 작성해서 실행해보자. 먼저 3을 입력해서 실행해보고 그 다음에는 0을 입력해서 실행해보자.
i = int(input('정수를 입력하세요: '))
print(f'15 / {i} = {15 / i}')
0을 입력하면 ZeroDivisionError 예외 객체를 생성한 것을 알 수 있다.
이처럼 오류가 발생하면 예외 객체(exception object)가 생성된다.
예외 객체는 주로 해당 오류와 관련된 기본 오류 메시지를 담고 있다.
따라서, 사용자가 0을 입력하면 ZeroDivisionError로 예외 처리를 하는 프로그램을 작성해보자.
i = int(input('정수를 입력하세요: '))
try: # 오류가 발생할 가능성이 있는 코드를 try문 안에 넣는다.
print(f'15 / {i} = {15 / i}')
except ZeroDivisionError:
print('{0}을 입력했습니다. 다른 숫자를 입력하세요.'.format(i))
예외가 발생하지 않았다. 0을 입력하면 ZeroDivisionError로 예외 처리를 했기 때문이다. 하지만 프로그램이 종료되었기 때문에 다시 프로그램을 실행해야 한다.
프로그램을 중단하지 않고 예외 처리를 할 수는 없을까?
무한 루프를 사용하면 된다.
이번에는 무한 루프를 사용해 사용자가 0을 입력하면 예외 처리를 하고, 다른 숫자를 입력할 때까지 다시 입력을 요구하는 프로그램을 작성해보자.
while True:
try: # 오류가 발생할 가능성이 있는 코드를 try문 안에 넣는다.
i = int(input('정수를 입력하세요: '))
print(f'15 / {i} = {15 / i}')
break # 유효한 값을 입력했으므로 처리한 후 무한 루프를 빠져나간다.
except ZeroDivisionError:
print('{}을 입력했습니다. 다른 숫자를 입력하세요.'.format(i))
그런데 만약 숫자가 아닌 문자를 입력하면 어떻게 될까?
이번에는 앞서 작성한 프로그램을 실행하고 문자 a를 입력해보자. 이번에는 숫자를 입력하지 않았기 때문에 ValueError 오류가 났다.
여러 종류의 예외를 한번에 처리할 수 없을까?
여러 개의 예외를 한번에 처리할 수 있다.
이번에는 ZeroDivisionError와 ValueError를 한꺼번에 처리하는 프로그램을 작성해보자.
# --- 복수의 except문
while True:
try: # 오류가 발생할 가능성이 있는 코드를 try문 안에 넣는다.
i = int(input('정수를 입력하세요: '))
print(f'15 / {i} = {15 / i}')
break # 유효한 값을 입력했으므로 처리한 후 무한 루프를 빠져나간다.
except ZeroDivisionError:
print('{}을 입력했습니다. 다른 숫자를 입력하세요.'.format(i))
except ValueError:
print('쑷자를 입력해야 합니다.')
여러 개의 예외를 하나의 그룹으로 묶어 처리할 수도 있다. 다음 프로그램을 작성해서 실행해보자.
# --- 예외 그룹
while True:
try: # 오류가 발생할 가능성이 있는 코드를 try문 안에 넣는다.
i = int(input('정수를 입력하세요: '))
print(f'15 / {i} = {15 / i}')
break # 유효한 값을 입력했으므로 처리한 후 무한 루프를 빠져나간다.
except (ZeroDivisionError, ValueError): # 반드시 괄호로 묶어야 한다.
print('잘못된 값이 입력되었습니다. 다시 입력하세요.')
예외 객체는 해당 오류와 관련된 기본 오류 메시지를 담고 있다. 따라서 예외 객체를 변수로 참조하면 해당 오류가 무엇인지 확인할 수 있다.
다음 프로그램은 예외 그룹에 변수를 지정해서 무슨 오류가 났는지를 확인 할 수 있도록 했다.
# --- as 변수
while True:
try: # 오류가 발생할 가능성이 있는 코드를 try문 안에 넣는다.
i = int(input('정수를 입력하세요: '))
print(f'15 / {i} = {15 / i}')
break # 유효한 값을 입력했으므로 처리한 후 무한 루프를 빠져나간다.
except (ZeroDivisionError, ValueError) as err: # 반드시 괄호로 묶어야 한다.
print('다음과 같은 예외가 발생했습니다:', err)
print('다시 입력하세요.')
try-except-else-finally문은 파이썬에서 제기하는 예외(exception)를 통해 흐름을 제어하는 일종의 조건문이다. if문과 달리 오직 ‘예외 발생’만을 조건으로 설정할 수 있다.
예외 처리를 하려면 try문 블록 안에 오류가 발생할 가능성이 있는 코드를 넣고 실행하면 된다.
예외를 처리하는 코드의 일반적인 형식은 다음과 같다.
try:
try-명령문
except 예외그룹-1 [as 변수-1]:
예외처리-명령문-1
except 예외그룹-2 [as 변수-2]:
예외처리-명령문-2
...
except 예외그룹-N [as 변수-N]:
예외처리-명령문-N
else:
else-명령문
finally:
finally-명령문
특징은 다음과 같다.
except문의 실행 순서는 다음과 같다.
예외가 발생하지 않고 프로그램이 정상적으로 종료하는 경우

예외가 발생하고 해당 예외를 처리하는 경우

예외가 발생했지만 해당 예외를 지정하지 않아 예외 처리를 실패한 경우

각 except문의 예외그룹 은 하나의 예외를 처리할 수도 있고, 튜플 형태의 예외 목록을 한번에 처리하는 것도 가능하다.
# --- 한 개의 예외만 처리한다.
try:
15 / 0
except ZeroDivisionError:
print('0으로 나눌 수 없습니다.')
# --- 복수의 예외를 처리한다.
try:
15 / 'a' # 0외 다른 문자도 입력해본다.
except (ZeroDivisionError, ValueError, TypeError, RuntimeError):
print('뭔가 잘못되었습니다.')
선택 사항인 as 변수 를 사용하면 발생한 예외 객체를 참조하기 때문에 해당 오류와 관련된 기본 오류 메시지를 확인할 수 있다.
# --- 한 개의 예외만 변수로 참조해서 처리한다.
try:
15 / 0
except ZeroDivisionError as err:
print('다음과 같은 예외가 발생했습니다:', err)
as 변수 를 사용하면 튜플 형태의 예외그룹 도 하나의 변수 로 참조할 수 있다.
# --- 복수의 예외를 변수로 참조해서 처리한다.
try:
15 / '0'
except (ZeroDivisionError, TypeError) as err:
print('다음과 같은 예외가 발생했습니다:', err)
except:문처리할 예외그룹 을 다음 예처럼 except:만 사용할 수 있다.
try:
15 / 0
except: # 모든 예외를 처리하기 때문에 권장하지 않는 방법이다.
print('뭔가 잘못된 것 같아요 ㅠㅠ')
try:
15 / 0
except Exception as err: # ZeroDivisionError가 더 바람직하다.
print(e)
발생하는 모든 오류를 처리하지만 이것은 좋은 방법이 아니니 권장하지 않는다. 가급적이면 발생 가능한 모든 오류를 구체적으로 지정할 것을 권장한다. 발생 가능한 오류가 무엇 무엇인지 모를 때만 어쩔 수 없이 이 방법을 사용해야 한다.
사용자가 입력한 값을 정수로 형변환을 시도한다. 이때 예외가 발생하면 ‘형식이 올바르지 않습니다’라는 문구를 출력하고, 예외가 발생하지 않으면 입력한 값을 출력한다. 반드시 try-except-else 문을 사용한다.
실행 예
> python exception_simple.py
자연수를 입력하세요...: a
형식이 올바르지 않습니다.
> python exception_simple.py
자연수를 입력하세요...: 5.0
형식이 올바르지 않습니다.
> python exception_simple.py
숫자나 문자를 입력하세요...: 5
5
while-else문 < Lab: 과일을 검색하는 while-else문 >과 for-else문 < Lab: 과일을 검색하는 for-else문 >으로 과일 검색을 한 것과 같은 결과를 가져오는 프로그램을 try-except-else문을 사용해서 구현해보자.
실행 예
> python fruits_exception.py
과일 이름을 입력하세요...: 블루베리
과일 목록의 4번째에 존재합니다.
> python fruits_exception.py
과일 이름을 입력하세요...: 수박
과일 목록에 존재하지 않습니다.
try:
15 / 0
except ArithmeticError as err:
print('산술 예외가 발생했습니다. =>', err)
except ZeroDivisionError: # 절대 실행되지 않는 except문이다
print('0으로 나눌 수 없습니다. =>', err)
0으로 나눴는데 왜 ZeroDivisionError가 아니라 ArithmeticError가 예외 처리를 할까?
except문을 작성할 때 주의할 점은 오류가 발생하면 각 except문이 순서대로 실행된다는 것이다. 뿐만 아니라 except문에 지정한 얘외그룹에 속한 예외 뿐만 아니라 그 하위 예외를 모두 처리한다. 이처럼 예외는 위계 관게가 존재하기 때문에 범위가 더 넓고 포괄적인 상위 예외를 먼저 선언하면, 하위 예외는 절대로 실행되지 않는다. 따라서 프로그램을 작성할 때 다음과 같이 좀 더 세부적인 하위 예외를 먼저 작성해야 한다.
try:
15 / 0
except ZeroDivisionError as err:
print('0으로 나눌 수 없습니다. =>', err)
except ArithmeticError as err:
print('산술 예외가 발생했습니다. =>', err)
결론 : 따라서 항상 하위 예외가 상위 예외보다 먼저 선언되어야 한다!!!
이처럼 예외는 상위 예외와 하위 예외로 이루어진 계층 구조를 가지고 있다.
다음 그림은 예외 계층 구조의 일부를 보여준다.

하위 예외로 갈수록 오류의 종류가 보다 구체적이며 상위 예외는 자신의 아래에 있는 모든 하위 예외를 처리할 수 있다. 하지만 예외 처리를 할 때 해당 오류에 맞는 구체적인 예외 처리를 하는 것이 좋기 때문에 가급적인면 상위 예외보다는 해당 예외를 지정해서 처리하는 것이 좋다.
파이썬이 다루는 모든 예외 종류와 계증 구조는 파이썬 문서를 확인하면 된다.
오류가 발생해도 처리하지 않고 그냥 통과시켜야 할 때도 종종 있다. 이런 상황에서는 except문에 예외 처리 코드를 넣지 않고 그냥 pass문을 추가하면 된다.
숫자를 0으로 나누었을 때 예외를 처리하지 않고 그냥 통과시켜보자.
try:
15 / 0
except ZeroDivisionError:
pass
오류가 발생해 작업을 처리하지 못하는 경우에도, 반드시 실행해야 하는 코드가 있을 때 사용한다. 컴퓨터나 프로그램이 충돌이 일어나 갑자기 멈추지 않는 한, try문 안의 오류 발생과 상관없이 finally문은 마지막에 반드시 실행된다.
try-finally문을 작성하는 형식은 다음과 같다.
try:
try-명령문
finally:
finally-명령문
숫자를 0으로 나누었을 때 예외 처리를 하지 않아도 finally문이 실행되는지 확인해보자.
try:
15 / 0
finally: # 반드시 처리해야 하는 코드는 finally문에 넣고 처리하면 된다.
print('예외가 발생해도 finally문은 반드시 실행된다!!!')
try-finally문은 주로 try문을 종료한 후, 웹 사이트나 데이터베이스 연결을 끊거나 현재 처리 중인 파일을 닫는 등 최종 정리(clean-up)를 위한 명령문을 작성할 때 유용하다.
프로그래밍을 하다보면 종종 일부러 예외를 발생시켜야 할 때도 있다. 예를 들어 꼭 구현해야 할 함수가 있지만 아직 구현하지 못했을 때, NotImplementedError라는 예외 클래스를 사용해서 고의로 예외를 발생시킬 수도 있다. 의도적으로 예외를 만들기 위해서는 raise문을 이용해서 예외를 강제로 발생시키면 된다.
def marry(girl, boy): # 결혼 함수를 정의한다.
raise NotImplementedError('marry() 함수는 다음 버전에서 구현할 예정입니다!!!')
# 예외 처리를 하지 않고 함수를 호출한다.
marry('네오', '프로도')
# 함수를 호출할 때 예외 처리를 한다.
try:
marry('네오', '프로도')
except NotImplementedError as err:
print(err)
raise문을 사용하는 형식은 다음과 같다.
<pre>raise 예외클래스[(전달인자)] [from 근원예외객체]</pre>
특징은 다음과 같다.
[따라해보기] raise문을 사용해서 예외를 생성해보자.
i = 0
if (i == 0):
raise ValueError # ValueError()와 같다.
[따라해보기] 이번에는 예외클래스 에 전달인자 룰 지정해보자. 전달인자 의 값이 예외 클래스의 이름과 함께 출력된다.
i = 0
if (i == 0):
raise ValueError('0이 입력되었습니다.')
[따라해보기] 전달인자 의 값을 문자열이 아닌 숫자로 해도 그 값 자체를 출력한다.
i = 0
if (i == 0):
raise ValueError(-3.14)
raise문 다음에 예외 클래스를 지정하지 않고 raise문만 사용하면, 현재 발생한 오류의 예외를 처리하지 않고 호출한 상위 명령문으로 그 오류를 넘길 때 사용한다.
numbers = [7, -5, 8, 3, 'a', 9]
total = 0
try:
for i in numbers:
try:
total += i
except TypeError:
print('예외가 발생했으니, 호출한 명령문에서 처리해주세요.')
raise # 예외 처리를 여기서 하지 않고 호출한 명령문으로 예외를 넘긴다.
except Exception as err:
print(err)
else:
print(total)
만약 오류가 없으면 raise문은 실행되지 않는다.
numbers = [7, -5, 8, 3, 1, 9]
total = 0
try:
for i in numbers:
try:
total += i
except TypeError:
print('예외가 발생했으니, 호출한 명령문에서 처리해주세요')
raise # 예외 처리를 여기서 하지 않고 호출한 명령문으로 예외를 넘긴다.
except Exception as err:
print(err)
else:
print(total)
현재 발생한 예외가 없으면 RuntimeError로 예외를 발생시킨다. 따라서, raise문을 일반 명령문에 사용하면 오류가 발생하지 않아도(현재 발생한 오류가 없어도), 프로그램 실행 도중 raise문에서 RuntimeError를 생성해서 오류를 발생시키기 때문에 주의해서 사용해야 한다.
total = 0
for i in range(1, 11): # 1에서 10까지 합을 구한다.
total += i
if total > 30:
print(total)
raise # 현재 발생한 예외가 없어도 RuntimeError 예외를 발생시킨다.
예외 처리를 통해 원하는 값의 범위를 입력하지 않으면 계속해서 입력을 요청하는 프로그램을 작성한다.
실행 예
> python infinite_loop_exception_handling.py
1-9 사이의 숫자를 입력하세요...: apple
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 블루베리
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 0
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 10
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: -1
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 1.234
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 5.0
ValueError: 1-9 사이의 자연수를 입력하세요!!!
1-9 사이의 숫자를 입력하세요...: 5
통과하셨습니다.
사용자가 입력한 값이 양의 정수면 그 수가 소수인지를 판별하는 프로그램을 구현해보자.
소수(prime numbers)란?
실행 예
> python is_prime_number.py
임의의 양의 정수를 입력하세요: a
ValueError: 1보다 큰 양의 정수를 입력하세요.
임의의 양의 정수를 입력하세요: -1
ValueError: 1보다 큰 양의 정수를 입력하세요.
임의의 양의 정수를 입력하세요: 1.0
ValueError: 1보다 큰 양의 정수를 입력하세요.
임의의 양의 정수를 입력하세요: 1
ValueError: 1보다 큰 양의 정수를 입력하세요.
임의의 양의 정수를 입력하세요: 125
이 숫자는 소수가 아닙니다.
5 x 25 = 125
> python is_prime_number.py
임의의 양의 정수를 입력하세요: 2
이 숫자는 소수입니다.
> python is_prime_number.py
임의의 양의 정수를 입력하세요: 19
이 숫자는 소수입니다.
https://docs.python.org/3/library/exceptions.html
BaseException
Exception
> StopIteration
> StopAsyncIteration
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
BufferError
EOFError
ImportError
ModuleNotFoundError
LookupError
IndexError
KeyError
MemoryError
NameError
UnboundLocalError
OSError (EnvironmentError / IOError / WindowsError)
BlockingIOError
ChildProcessError
ConnectionError
BrokenPipeError
ConnectionAbortedError
ConnectionRefusedError
ConnectionResetError
FileExistsError
FileNotFoundError
InterruptedError
IsADirectoryError
NotADirectoryError
PermissionError
ProcessLookupError
TimeoutError
ReferenceError
RuntimeError
NotImplementedError
RecursionError
SyntaxError
IndentationError
TabError
SystemError
TypeError
ValueError
UnicodeError
UnicodeDecodeError
UnicodeEncodeError
UnicodeTranslateError
Warning
DeprecationWarning
PendingDeprecationWarning
RuntimeWarning
SyntaxWarning
UserWarning
FutureWarning
ImportWarning
UnicodeWarning
BytesWarning
ResourceWarning
> GeneratorExit
> KeyboardInterrupt
> SystemExit
사전 조건(precondition)이나 사후 조건(postcondition)을 확인할 때 가정 설정문을 사용할 수 있다.
가정 설정문을 작성하는 일반적인 형식은 다음과 같다.
<pre>assert 불린-표현식, [추가-표현식]</pre>
특징은 다음과 같다.
assert문은 주로 코드를 테스트하거나 디버깅할 때 유용하다.
가정 설정문은 주로 테스트나 디버깅 때 사용하기 때문에 이러한 작업이 끝난 production 코드에서는 가정 설정문을 주로 비활성화한다.
방법 1
방법 2
방법 3
예시 : 가정 설정문
# --- PESSIMISTIC approach
def product(*args):
assert all(args), '0(zero) 전달인자'
result = 1
for arg in args:
result *= arg
return result
product(2, 3, 0, 5)
# --- OPTIMISTIC approach
def product(*args):
result = 1
for arg in args:
result *= arg
assert all(args), '0(zero) 전달인자'
return result
product(2, 3, 0, 5)
THE END