Python 강좌 : 제 18강 - 예외 형식
예외 형식(try-except-finally)
예외 형식(try-except-finally) 구문을 이용해 오류가 발생하는 부분을 처리할 수 있습니다.
특정 오류가 발생할 때, 이를 처리해 알고리즘이 원활하게 구동할 수 있도록 구현할 수 있습니다.
또한, 특정 오류가 발생했음을 사용자에게 알려주어 입력 데이터를 조건에 맞게 입력할 수 있게 합니다.
Python에서는 실행 중에 발생하는 오류를 예외(Exception)라고 하며, 처리되지 않은 예외는 프로그램을 중지시킵니다.
오류는 크게 두 종류로 나눌 수 있습니다. 콜론을 빠뜨리거나 괄호를 닫지 않은 구문 오류(SyntaxError)는 코드를 실행하기 전에 발견되므로 코드를 고쳐야만 해결됩니다. 반면 예외는 문법은 올바르지만 실행 도중의 상황 때문에 발생합니다. 사용자가 숫자 대신 문자를 입력하거나, 0으로 나누거나, 없는 파일을 열려는 경우처럼 코드만으로는 미리 막을 수 없는 상황이 대표적이며, 예외 형식은 이런 상황에 대비하는 문법입니다.
이번 강좌에서는 예외가 발생했을 때 실행이 어디로 이동하는지, 어떤 예외를 잡아야 하는지를 중심으로 try, except, else, finally의 동작을 살펴보고, 여러 예외를 다루는 방법과 Python 3.11 이후 추가된 기능을 다룹니다.
예외가 발생하면
print("시작")
number = int("abc")
print("끝")- 결과
- 시작
Traceback (most recent call last):
File “s.py”, line 2, in <module>
number = int(“abc”)
ValueError: invalid literal for int() with base 10: ‘abc’
예외를 처리하지 않았을 때의 기본 동작입니다.
print("시작")은 정상적으로 실행되지만, 두 번째 줄의 int("abc")에서 ValueError가 발생하는 순간 프로그램이 즉시 중지됩니다. 그 아래의 print("끝")은 실행되지 않습니다.
Python은 중지하면서 트레이스백(Traceback)이라는 오류 보고서를 출력합니다. 읽는 방법은 다음과 같습니다.
File "s.py", line 2: 예외가 발생한 파일과 줄 번호입니다. 함수 안에서 발생했다면 호출 경로가 위에서 아래로 차례대로 표시됩니다.number = int("abc"): 예외가 발생한 코드입니다.ValueError: ...: 마지막 줄은 예외의 종류와 메시지이며, 원인을 파악하는 데 가장 중요한 정보입니다.
예외 처리는 이 “즉시 중지”를 막고, 예외가 발생했을 때 실행할 다른 경로를 지정하는 것입니다.
- Tip : 파일 경로는 실행 환경에 따라 다르게 표시됩니다. 위 결과는 경로를 줄여 표기했습니다.
try - except - finally
while True:
a = input("숫자입력 : ")
try:
print(int(a))
except:
print("숫자가 아닙니다.")
finally:
print("최종 :", a)
print("---------------------")- 결과
- 숫자입력 : 123
123
최종 : 123
———————
숫자입력 : alpha
숫자가 아닙니다.
최종 : alpha
———————
while True:를 이용하여 일정 구문을 반복합니다. 반복을 멈추려면 Ctrl + C를 입력합니다.
try: 구문은 먼저 실행되는 구문입니다. 실행 도중 오류가 발생하면 try 구문의 나머지 부분은 실행되지 않고 바로 except 구문으로 이동합니다.
except: 구문은 try 구문에서 오류가 발생했을 때 실행되는 구문입니다.
finally: 구문은 오류 발생 여부와 무관하게 항상 마지막에 실행되는 구문입니다.
try-except-finally 구문을 활용하여 int("alpha")에서 발생하는 ValueError 등으로 프로그램이 중지되는 것을 방지할 수 있습니다.
두 번의 입력을 따라가 보면 다음과 같습니다.
123을 입력하면try의int(a)가 성공하여123을 출력합니다. 예외가 없으므로except는 건너뛰고finally가 실행됩니다.alpha를 입력하면int(a)에서ValueError가 발생합니다.print()는 실행되지 않고except로 이동하여 안내 문구를 출력한 뒤,finally가 실행됩니다.- 두 경우 모두
try문 전체가 끝나면 그 아래의 구분선 출력으로 이어지며, 프로그램은 중지되지 않고 다음 입력을 기다립니다.
-
Tip :
finally구문은 필수요소가 아니며, 불필요한 경우 사용하지 않아도 됩니다. 주로 파일 닫기, 연결 해제 등 정리 작업에 사용합니다. -
Tip : 예외 종류를 생략한
except:는Ctrl + C로 발생하는KeyboardInterrupt까지 모두 잡기 때문에 권장하지 않습니다.except ValueError:처럼 처리할 예외를 명시하거나, 최소한except Exception:을 사용합니다.
실행 흐름의 이동
print("[1] try 이전")
try:
print("[2] 변환 시도")
number = int("abc")
print("[3] 변환 성공")
except ValueError:
print("[4] except 실행")
print("[5] try 문 다음")- 결과
- [1] try 이전
[2] 변환 시도
[4] except 실행
[5] try 문 다음
예외가 발생했을 때 실행 위치가 어떻게 이동하는지 번호를 붙여 확인하는 예제입니다.
try 블록은 위에서부터 한 줄씩 정상적으로 실행되다가, int("abc")에서 예외가 발생하는 순간 블록의 나머지([3])를 버리고 아래의 except 절을 찾아갑니다.
except ValueError:가 발생한 예외와 일치하므로 그 블록([4])을 실행합니다. 예외가 처리되었으므로 프로그램은 정상 상태로 돌아가고, 실행은 try 문 전체의 다음 줄([5])로 이어집니다.
중요한 점은 except를 실행한 뒤 예외가 발생한 지점으로 되돌아가지 않는다는 것입니다. 건너뛴 [3]은 다시 실행되지 않으므로, 예외 이후의 코드가 꼭 실행되어야 한다면 try 문 바깥이나 finally에 작성해야 합니다.
def convert(text):
print("convert 시작")
result = int(text)
print("convert 끝")
return result
def process(text):
print("process 시작")
value = convert(text)
print("process 끝")
return value * 2
try:
print(process("abc"))
except ValueError as e:
print("최상위에서 처리 :", e)- 결과
- process 시작
convert 시작
최상위에서 처리 : invalid literal for int() with base 10: ‘abc’
예외는 발생한 곳에서 처리되지 않으면 자신을 호출한 곳으로 전달됩니다. 이를 예외 전파(Propagation)라고 합니다.
호출 순서는 try 블록 → process() → convert()입니다. convert() 안의 int(text)에서 예외가 발생했지만 convert()에는 try가 없으므로 함수가 즉시 중단되고, 예외는 convert()를 호출한 process()로 전달됩니다.
process()에도 try가 없으므로 마찬가지로 중단되어 예외가 바깥으로 전달되며, 최종적으로 가장 바깥의 except ValueError에서 처리됩니다. 그 사이의 convert 끝, process 끝과 print() 호출은 모두 실행되지 않습니다.
이처럼 예외는 처리해 줄 except를 만날 때까지 호출 경로를 거슬러 올라가며, 끝까지 만나지 못하면 프로그램이 중지되고 트레이스백이 출력됩니다. 따라서 예외는 반드시 발생한 함수 안에서 처리할 필요가 없으며, 처리 방법을 결정할 수 있는 위치에서 처리하면 됩니다.
- Tip : 함수는 제 20강에서 자세히 다룹니다.
특정 예외 처리
while True:
a = input("숫자입력 : ")
try:
print(int(a))
except ValueError:
try:
print(int(float(a)))
except ValueError:
print("오류 발생")
print("---------------------")- 결과
- 숫자입력 : 123.123
123
———————
숫자입력 : 윤대희
오류 발생
———————
특정 오류에 대해서 except 오류사항:을 통해 예외처리할 수 있습니다.
ValueError에 대해서만 예외처리할 경우, 다른 오류가 발생한다면 프로그램이 중지됩니다.
123.123을 입력하면 바깥 try의 int(a)가 실패하여 바깥 except로 이동하고, 그 안의 int(float(a))가 성공하여 123이 출력됩니다. 윤대희를 입력하면 float() 변환도 실패하므로 안쪽 except까지 이동하여 오류 발생이 출력됩니다.
예외 처리를 해도, except 구문 안에서도 오류가 발생할 수 있습니다.
예제와 같이 구문이 복잡해지지 않도록 하나의 예외 처리사항 안에서 문제를 해결하도록 구성합니다. 위 예제는 처음부터 int(float(a)) 하나만 try에 두면 중첩 없이 같은 결과를 얻을 수 있습니다.
어떤 예외를 잡아야 하는가
except에 지정하는 예외의 범위는 넓을수록 편해 보이지만, 넓게 잡을수록 예상하지 못한 오류까지 조용히 숨기는 문제가 생깁니다.
def average(L):
try:
return sum(L) / lenn(L)
except Exception:
return 0
print(average([1, 2, 3]))
print(average([]))- 결과
- 0
0
빈 목록일 때 0으로 나누는 ZeroDivisionError를 막으려고 except Exception:을 사용한 예제입니다.
하지만 len을 lenn으로 잘못 작성했기 때문에 실제로는 모든 호출에서 NameError가 발생하고 있습니다. except Exception:이 이 오류까지 잡아 0을 반환하므로, [1, 2, 3]의 평균이 0으로 출력되는데도 아무 오류 메시지가 없습니다.
이런 버그는 프로그램이 멈추지 않기 때문에 잘못된 결과가 계속 쌓인 뒤에야 발견되며, 원인을 찾기도 어렵습니다.
def average(L):
try:
return sum(L) / lenn(L)
except ZeroDivisionError:
return 0
print(average([1, 2, 3]))- 결과
- NameError: name ‘lenn’ is not defined. Did you mean: ‘len’?
처리하려던 예외인 ZeroDivisionError만 지정하면, 예상하지 못한 NameError는 잡히지 않고 그대로 드러나므로 오타를 즉시 발견할 수 있습니다.
예외를 잡는 기준은 다음과 같습니다.
- 발생할 것을 알고 있고, 처리 방법이 정해진 예외만 잡습니다. 변환 실패면 다시 입력받기, 0으로 나누기면
0반환처럼 구체적인 대응이 있어야 합니다. - 처리 방법이 없는 예외는 잡지 않고 그대로 전파되게 둡니다. 버그는 숨기는 것보다 드러나는 것이 낫습니다.
try블록에는 예외가 발생할 수 있는 최소한의 코드만 둡니다. 블록이 넓으면 의도하지 않은 줄에서 발생한 같은 종류의 예외까지 잡게 됩니다.
- Tip :
except Exception:은 프로그램의 가장 바깥에서 예상하지 못한 오류를 기록(로그)한 뒤 종료하는 용도처럼, 모든 오류를 받아야 하는 특별한 위치에서만 사용합니다.
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(KeyboardInterrupt, Exception))
print(KeyboardInterrupt.__mro__)- 결과
- True
True
False
(<class ‘KeyboardInterrupt’>, <class ‘BaseException’>, <class ‘object’>)
예외는 계층 구조를 이루며, except 예외:는 지정한 예외와 그 하위 예외를 모두 잡습니다. issubclass(A, B)는 A가 B의 하위 예외인지 확인합니다.
대부분의 예외는 Exception의 하위이므로 except Exception:은 ValueError도 잡습니다. KeyError와 IndexError는 LookupError의 하위이므로 except LookupError: 하나로 두 예외를 모두 처리할 수 있습니다.
KeyboardInterrupt(Ctrl + C)와 SystemExit(프로그램 종료 요청)는 Exception이 아닌 최상위 BaseException의 직접 하위입니다. 그래서 except Exception:으로는 잡히지 않지만, 예외 종류를 생략한 except:는 BaseException 전체를 잡으므로 프로그램을 끄는 동작까지 막아 버립니다.
data = {"a": 1}
for key in ["a", "b"]:
try:
print(data[key])
except LookupError:
print("LookupError :", key)
except KeyError:
print("KeyError :", key)- 결과
- 1
LookupError : b
except 절이 여러 개라면 위에서부터 차례대로 비교하여 처음 일치하는 하나만 실행합니다.
data["b"]에서 KeyError가 발생하지만, 첫 번째 except LookupError:가 KeyError의 상위 예외이므로 먼저 일치하여 실행됩니다. 아래의 except KeyError:는 절대 실행되지 않습니다.
그러므로 여러 except를 작성할 때는 구체적인(하위) 예외를 위에, 넓은(상위) 예외를 아래에 배치해야 합니다.
try - except - else - finally
def to_int(text):
try:
number = int(text)
except ValueError as e:
print("숫자가 아닙니다 :", e)
else:
print("변환 성공 :", number)
finally:
print("최종 :", text)
to_int("123")
to_int("alpha")- 결과
- 변환 성공 : 123
최종 : 123
숫자가 아닙니다 : invalid literal for int() with base 10: ‘alpha’
최종 : alpha
else: 구문은 try 구문에서 오류가 발생하지 않았을 때만 실행되는 구문입니다.
실행 순서는 다음과 같습니다.
- 오류가 없는 경우 :
try→else→finally - 오류가 있는 경우 :
try(오류 지점까지) →except→finally
except 예외 as 변수:를 사용하면 발생한 예외 객체를 변수로 받아 오류 메시지 등을 확인할 수 있습니다.
else에 작성한 코드는 try의 마지막에 작성한 것과 비슷해 보이지만 차이가 있습니다. else 블록에서 발생한 예외는 같은 try 문의 except에서 잡히지 않습니다. 따라서 else를 사용하면 except가 처리하는 범위를 int(text) 한 줄로 정확히 한정할 수 있습니다.
-
Tip : 오류가 발생할 수 있는 코드만
try에 두고, 성공했을 때의 처리는else에 두면 의도하지 않은 예외까지 잡는 실수를 줄일 수 있습니다. -
Tip : Python 3.14 이상에서는
finally구문 안에서return,break,continue를 사용하면SyntaxWarning이 발생합니다.finally의return은 진행 중인 예외를 무시해버리기 때문입니다.
def f():
try:
print("try 실행")
return "반환값"
finally:
print("finally 실행")
print(f())- 결과
- try 실행
finally 실행
반환값
finally는 try 블록이 return으로 함수를 빠져나가는 경우에도 실행됩니다.
return "반환값"을 만나면 반환할 값은 정해지지만, 함수를 실제로 빠져나가기 직전에 finally 블록이 먼저 실행됩니다. 그래서 finally 실행이 출력된 뒤에 호출한 곳으로 "반환값"이 전달되어 출력됩니다. break, continue로 반복문을 빠져나가는 경우도 마찬가지입니다.
이 성질 때문에 finally는 함수가 어떤 경로로 끝나든 반드시 수행해야 하는 정리 작업에 적합합니다.
try:
int("abc")
except ValueError as e:
print(type(e))
print(type(e).__name__)
print(e.args)- 결과
- <class ‘ValueError’>
ValueError
(“invalid literal for int() with base 10: ‘abc’”,)
as e로 받은 예외 객체에서 예외의 종류와 정보를 꺼낼 수 있습니다.
type(e)는 예외의 클래스, type(e).__name__은 예외 이름의 문자열이며, e.args에는 예외를 만들 때 전달된 인수가 튜플로 저장됩니다. print(e)나 str(e)는 이 메시지를 문자열로 보여줍니다.
- Tip :
as로 받은 변수는except블록이 끝나면 자동으로 삭제됩니다. 블록 바깥에서e를 사용하면NameError: name 'e' is not defined가 발생하므로, 나중에 필요하다면 블록 안에서 다른 변수에 저장합니다.
다중 예외 처리
L = [1, 2, 3]
for idx in [1, 5, "a"]:
try:
print(L[idx])
except (IndexError, TypeError) as e:
print("잘못된 인덱스 :", idx, type(e).__name__)
for idx in [1, 5, "a"]:
try:
print(L[idx])
except IndexError, TypeError:
print("잘못된 인덱스 :", idx)- 결과
- 2
잘못된 인덱스 : 5 IndexError
잘못된 인덱스 : a TypeError
2
잘못된 인덱스 : 5
잘못된 인덱스 : a
여러 예외를 한 번에 처리하려면 except (예외1, 예외2):처럼 튜플로 묶어 작성합니다.
L[5]는 범위를 벗어나 IndexError가, L["a"]는 인덱스로 문자열을 사용해 TypeError가 발생합니다. 두 예외 모두 튜플에 포함되어 있으므로 같은 except에서 처리되며, type(e).__name__으로 실제로 어떤 예외였는지 구분할 수 있습니다.
Python 3.14 이상에서는 PEP 758에 따라 except 예외1, 예외2:처럼 괄호를 생략할 수 있습니다.
단, as를 함께 사용하는 경우에는 괄호를 생략할 수 없으며, except IndexError, TypeError as e:는 SyntaxError가 발생합니다.
- Tip : 예외마다 다른 처리가 필요하다면
except IndexError:,except TypeError:처럼except구문을 여러 개 작성합니다. 위에서부터 순서대로 검사하여 처음 일치하는 구문 하나만 실행됩니다.
예외 처리 중의 예외
try:
int("abc")
except ValueError:
raise RuntimeError("설정 파일을 읽을 수 없습니다")- 결과
- Traceback (most recent call last):
File “s.py”, line 2, in <module>
int(“abc”)
~~~^^^^^^^
ValueError: invalid literal for int() with base 10: ‘abc’
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File “s.py”, line 4, in <module>
raise RuntimeError(“설정 파일을 읽을 수 없습니다”)
RuntimeError: 설정 파일을 읽을 수 없습니다
raise 예외는 예외를 직접 발생시키는 구문입니다. 예제는 except 블록 안에서 새로운 예외를 발생시켰습니다.
except 블록 안에서 발생한 예외는 같은 try 문의 다른 except로 가지 않고 바깥으로 전파됩니다. 이때 Python은 처리 중이던 원래 예외(ValueError)를 버리지 않고 함께 보여주므로, 트레이스백이 두 부분으로 출력되고 그 사이에 During handling of the above exception, another exception occurred: 문장이 표시됩니다.
이 덕분에 최종 오류(RuntimeError)뿐만 아니라 그 원인이 된 오류까지 추적할 수 있습니다.
- Tip :
raise로 예외를 발생시키는 방법과 사용자 정의 예외는 제 30강에서 자세히 다룹니다.
예외 메모(add_note)
def parse(text):
try:
return int(text)
except ValueError as e:
e.add_note(f"입력값 : {text!r}")
raise
try:
parse("abc")
except ValueError as e:
print(e)
print(e.__notes__)- 결과
- invalid literal for int() with base 10: ‘abc’
[“입력값 : ‘abc’”]
예외.add_note(문자열)을 사용하면 예외 객체에 추가 정보(메모)를 덧붙일 수 있습니다. (Python 3.11 이상)
추가한 메모는 __notes__ 속성에 리스트로 저장되며, 예외가 처리되지 않아 출력될 때는 오류 메시지 아래에 함께 표시됩니다.
except 구문 안에서 raise만 작성하면 처리 중인 예외를 다시 발생시킵니다.
parse() 안의 except는 예외를 해결하지 않고 메모만 덧붙여 그대로 다시 발생시키므로, 예외는 호출한 곳의 except로 전파됩니다. 예외의 종류와 원래 메시지는 바뀌지 않으며 __notes__에 메모만 추가됩니다.
이처럼 예외가 발생한 위치에서는 처리 방법을 모르지만 원인 파악에 도움이 되는 정보를 알고 있을 때 사용합니다.
예외 그룹(ExceptionGroup)
errors = [ValueError("값 오류"), TypeError("형식 오류"), KeyError("k")]
try:
raise ExceptionGroup("여러 오류", errors)
except* ValueError as eg:
print("ValueError 처리 :", eg.exceptions)
except* (TypeError, KeyError) as eg:
print("TypeError/KeyError 처리 :", eg.exceptions)- 결과
- ValueError 처리 : (ValueError(‘값 오류’),)
TypeError/KeyError 처리 : (TypeError(‘형식 오류’), KeyError(‘k’))
ExceptionGroup(메시지, 예외 목록)은 여러 개의 예외를 하나로 묶어 발생시키는 예외입니다. (Python 3.11 이상)
여러 작업을 동시에 실행하는 경우처럼 오류가 동시에 여러 개 발생할 수 있을 때 사용합니다.
except* 구문은 예외 그룹 안에서 일치하는 예외만 골라 처리합니다.
일반 except와 달리, 일치하는 except* 구문이 모두 실행되며 각 구문의 eg.exceptions에는 해당하는 예외들만 담겨 있습니다.
예제에서는 그룹 안의 세 예외 중 ValueError가 첫 번째 except*로 분리되어 처리되고, 남은 TypeError와 KeyError가 두 번째 except*에서 처리됩니다. 일반 except였다면 처음 일치하는 절 하나만 실행되었을 것입니다.
- Tip : 하나의
try문에서except와except*를 섞어 쓸 수 없습니다. 처리되지 않고 남은 예외가 있다면 그 예외들만 다시 예외 그룹으로 묶여 발생합니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
try: |
예외가 발생할 수 있는 코드 | 예외 발생 시 나머지를 건너뛰고 except로 이동 |
except 예외: |
지정한 예외와 하위 예외를 처리 | 위에서부터 비교하여 처음 일치하는 하나만 실행 |
except 예외 as e: |
예외 객체를 변수로 받음 | e는 블록이 끝나면 삭제됨 |
except (예외1, 예외2): |
여러 예외를 한 번에 처리 | Python 3.14 이상은 as 없이 괄호 생략 가능 |
else: |
예외가 없을 때만 실행 | 여기서 발생한 예외는 같은 try의 except가 잡지 않음 |
finally: |
항상 마지막에 실행 | return, break로 빠져나갈 때도 실행 |
raise |
예외를 발생시키거나 다시 발생 | 처리 중 발생한 예외는 원래 예외와 함께 표시 |
예외.add_note() |
예외에 메모 추가 | Python 3.11 이상 |
except* |
예외 그룹에서 일치하는 예외만 처리 | Python 3.11 이상, 일치하는 절이 모두 실행 |
예외가 발생하면 실행은 현재 블록을 버리고 일치하는 except를 찾아 바깥으로 이동하며, 처리된 뒤에는 try 문 다음 줄로 이어집니다. 그리고 except에는 처리 방법을 알고 있는 구체적인 예외만 지정하여, 예상하지 못한 버그가 숨지 않도록 합니다.
댓글 남기기