멈추는 오류
잘 만든 프로그램도 사용자가 입력한 값 하나 때문에 멈춥니다.
나이를 입력받는 프로그램입니다. 사용자가 「스물」이라고 입력하면 어떻게 될까요?
print("시작")
age = int(input("나이: ")) # 「스물」을 입력하면 여기서 멈춘다
print("입력받은 나이:", age)
print("끝") # 멈추면 이 줄은 아예 실행되지 않는다
# 입력이 「스물」일 때
# 시작
# 나이: 스물
# ValueError: invalid literal for int() with base 10: '스물'"스물"은 정수로 변환할 수 없는 문자열입니다. 파이썬은 그 줄에서 실행을 멈추고 오류 메시지를 출력합니다. 그 뒤의 줄은 아예 실행되지 않습니다.
코드가 틀린 것이 아닙니다
int(input())은 잘못 쓴 코드가 아닙니다. 사용자가 입력한 값이 예상 밖이었을 뿐입니다. 이런 상황은 코드를 아무리 잘 작성해도 없앨 수 없습니다. 그러므로 이런 상황에서 무엇을 할지 미리 정해 두는 방법을 이번 시간에 배웁니다.
프로그램을 멈추게 하는 오류는 두 종류이고, 이번에 다루는 것은 그중 하나입니다. 멈추지 않는 오류까지 더하면 오류는 모두 세 종류입니다.
# 1) 문법 오류 — 실행되기 「전에」 발견된다
# if x > 3
# print("큼") # SyntaxError. 콜론이 없다
# 2) 실행 오류 — 실행되다가 「그 줄에서」 멈춘다
nums = [1, 2, 3]
print(nums[5]) # IndexError: list index out of range예외(exception)
프로그램이 실행되는 중에 발생하는 오류로, 처리하지 않으면 프로그램이 그 줄에서 멈춥니다. 콜론을 빠뜨린 것 같은 문법 오류는 try로 잡을 수 없습니다. 실행이 시작되기도 전에 발견되므로 try가 실행될 기회조차 없기 때문입니다.
멈추지 않는 세 번째 오류
프로그램이 끝까지 멈추지 않고 실행되었는데 결과만 틀린 경우가 있습니다. 평균을 구하면서 개수로 나누지 않고 합만 반환한 코드가 그렇습니다. 이것을 논리 오류라고 합니다. 파이썬은 코드에 쓴 대로 실행했을 뿐이므로 아무것도 알려 주지 않고, 예외도 발생하지 않으므로 try로 잡을 수도 없습니다. 정답을 미리 알고 있는 값을 넣어 실행 결과와 비교해 보아야 찾을 수 있습니다.
스스로 확인
print(nums[5])는 문법이 틀린 곳이 없는데도 멈춥니다. 콜론을 빠뜨린 코드와 이 코드는 멈추는 시점이 어떻게 다를까요?
잡아서 이어 가기
멈추는 대신 무엇을 할지 코드로 써 둡니다.
오류가 날 수 있는 코드를 try와 except로 감싸면 예외가 발생해도 프로그램이 멈추지 않고 except 안의 코드를 실행합니다. 이처럼 예외가 발생했을 때 할 일을 미리 정해 두는 것을 예외 처리(exception handling)라고 합니다.
try:
age = int(input("나이: "))
print("입력받은 나이:", age)
except ValueError:
print("숫자로 입력해 주세요")
print("끝") # 예외를 잡아 처리했으므로 이 줄도 실행된다
# 입력이 「스물」일 때
# 나이: 스물
# 숫자로 입력해 주세요
# 끝| 위치 | 언제 실행되나 |
|---|---|
try: 안 |
언제나 실행을 시작한다. 오류가 난 줄에서 실행을 중단하고 남은 줄은 건너뛴다 |
except X: 안 |
try 안에서 X 오류가 났을 때만 |
else: 안 |
try 안이 오류 없이 끝났을 때만 |
| 그 뒤의 줄 | 오류를 잡았든 오류가 없었든 이어서 |
try 안은 짧게 씁니다
try 안에 열 줄을 넣으면 그중 어느 줄에서 오류가 났는지 알기 어렵습니다. 오류가 날 수 있는 줄만 감싸는 것이 좋습니다.
오류의 종류마다 다르게 처리할 수 있습니다.
nums = [10, 0, 30]
try:
i = int(input("인덱스: "))
print(100 / nums[i])
except ValueError:
print("숫자로 입력해 주세요")
except IndexError:
print("없는 인덱스입니다. 0부터 2까지 입력해 주세요")
except ZeroDivisionError:
print("그 인덱스의 값이 0이라 나눌 수 없습니다")셋을 입력하면 ValueError, 5를 입력하면 IndexError를 처리하는 except로 갑니다. 1을 입력하면 인덱스 1의 값이 0이므로 ZeroDivisionError를 처리하는 except로 갑니다. 0을 입력하면 10.0이 출력됩니다. -1처럼 음수를 입력하면 뒤에서부터 세는 인덱스로 해석되어 오류 없이 3.3333333333333335가 출력되고, -4처럼 그 범위까지 벗어나야 IndexError가 납니다.
예외가 발생하면 파이썬은 except를 위에서부터 차례로 살펴 예외의 종류와 비교합니다. 종류가 맞는 except를 만나면 그 하나만 실행하고 나머지는 건너뜁니다. 맞는 것이 하나도 없으면 예외를 처리하지 않은 것과 같아서 그 줄에서 멈춥니다.
| 예외 이름 | 언제 발생하나 |
|---|---|
ValueError |
자료형은 맞지만 값이 알맞지 않을 때 — int("스물") |
IndexError |
리스트에 없는 인덱스를 꺼낼 때 |
KeyError |
딕셔너리에 없는 키를 꺼낼 때 |
ZeroDivisionError |
0으로 나눌 때 |
FileNotFoundError |
없는 파일을 열려고 할 때 |
TypeError |
그 자료형에 쓸 수 없는 연산을 할 때 |
반복문과 함께 쓰면 올바른 값을 입력할 때까지 다시 입력받는 프로그램이 됩니다.
while True:
try:
age = int(input("나이: "))
break # 여기까지 실행됐다면 숫자가 제대로 입력된 것이다
except ValueError:
print("숫자로 입력해 주세요")
print("나이는", age)break는 오류가 나지 않았을 때만 실행됩니다. 오류가 나면 바로 except로 넘어가므로 break는 실행되지 않고 반복을 한 번 더 합니다.
스스로 확인
try 안에서 break를 except 뒤로 옮기면 어떻게 될까요?
좁게 잡기
어떤 오류를 잡을지 쓰지 않으면 모든 오류가 잡힙니다.
except:라고만 쓰면 모든 오류가 잡힙니다. 편해 보이지만 위험합니다.
nums = [10, 20, 30]
# 넓게 잡으면 「무엇이 잘못됐는지」가 사라진다
try:
i = int(input("인덱스: "))
print(100 / nums[i])
except:
print("오류가 났습니다") # 어느 오류인지 알 수 없다
# 오타로 난 NameError까지 여기서 잡힌다. 고칠 곳을 찾을 수 없게 된다고칠 곳을 감춥니다
nums를 nusm으로 잘못 쓰면 NameError가 납니다. 이것은 사용자의 잘못이 아니라 코드를 작성한 내 잘못이고, 프로그램이 멈춰서 알려 주어야 고칠 수 있습니다. 모든 오류를 잡으면 「오류가 났습니다」만 출력되어 어디를 고쳐야 할지 알 수 없게 됩니다.
잡을 오류를 고르는 기준
「미리 예상할 수 있고, 발생했을 때 무엇을 할지 정해 둔 오류」만 잡습니다. 그 밖의 오류는 멈추게 두는 편이 낫습니다. 프로그램이 멈추는 것은 실패가 아니라 고칠 곳을 알려 주는 신호입니다.
파일을 다룰 때가 예외 처리를 쓰는 대표적인 경우입니다. 파일이 있는지 없는지는 프로그램이 정할 수 없기 때문입니다.
try:
with open("성적.csv", "r", encoding="utf-8") as f:
print(f.read())
except FileNotFoundError:
print("성적.csv를 찾을 수 없습니다")def read_scores(path):
"""파일에서 이름과 점수를 읽어 딕셔너리로 반환한다."""
table = {}
with open(path, "r", encoding="utf-8") as f:
for line in f:
values = line.strip().split(",")
table[values[0]] = int(values[1])
return table
try:
scores = read_scores("성적.csv")
except FileNotFoundError:
print("성적.csv가 없습니다. 파일을 먼저 만들어 주세요")
except ValueError:
print("점수 열에 숫자가 아닌 값이 있습니다")
else:
for name, score in scores.items():
print(f"{name}: {score}")
# 학생 A: 90
# 학생 B: 85파일이 없을 때와 점수가 숫자가 아닐 때를 따로 잡아 각각 무엇이 잘못됐는지 알려 줍니다. 오류가 나지 않았을 때만 할 일은 else에 두어 try 안을 짧게 유지했습니다.
스스로 확인
파일을 여는 첫 번째 코드에서 except FileNotFoundError를 except:로 바꾸면, f.read()를 f.raed()로 잘못 쓴 오타도 「성적.csv를 찾을 수 없습니다」를 출력합니다. 왜 그것이 문제일까요?
finally와 raise
오류가 나든 나지 않든 할 일은 어디에 두고, 파이썬이 알아채지 못하는 오류는 어떻게 알릴까요?
try 문의 끝에 finally:를 붙이면 그 안의 코드는 try 안에서 예외가 발생했든 발생하지 않았든 마지막에 반드시 실행됩니다. 결과와 상관없이 해야 하는 마무리를 여기에 둡니다.
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("0으로 나눌 수 없습니다")
return None
else:
return result
finally:
print("나누기를 마칩니다") # return보다 먼저, 어느 경우에나 실행된다
print(divide(10, 4))
print(divide(10, 0))
# 나누기를 마칩니다
# 2.5
# 0으로 나눌 수 없습니다
# 나누기를 마칩니다
# Nonedivide(10, 4)는 예외 없이 else 안의 return result에 이릅니다. 그런데 2.5보다 「나누기를 마칩니다」가 먼저 출력됩니다. 함수가 값을 반환하고 빠져나가기 직전에 finally 안이 실행되기 때문입니다. divide(10, 0)은 except 안에서 안내 문구를 출력한 뒤 return None에 이르고, 이때도 반환하기 전에 finally 안이 실행됩니다.
| 경우 | 실행 순서 |
|---|---|
| 예외가 나지 않았을 때 | try 안 → else 안 → finally 안 → 그 뒤의 줄 |
except에 쓴 종류의 예외가 났을 때 |
try 안(오류가 난 줄까지) → 그 except 안 → finally 안 → 그 뒤의 줄 |
except에 쓰지 않은 종류의 예외가 났을 때 |
try 안(오류가 난 줄까지) → finally 안 → 멈춘다. 그 뒤의 줄은 실행되지 않는다 |
return으로 함수를 빠져나갈 때 |
반환하기 직전에 finally 안 |
else와 finally는 다릅니다
else 안은 try 안이 오류 없이 끝났을 때만 실행되고, finally 안은 오류가 났을 때도 실행됩니다. 오류가 없을 때만 할 일은 else에, 어느 경우에나 할 일은 finally에 둡니다.
지금까지 본 예외는 모두 파이썬이 발생시켰습니다. raise 뒤에 예외의 종류와 메시지를 쓰면 프로그래머가 직접 예외를 발생시킬 수 있습니다.
def check_age(age):
if age < 0:
raise ValueError("나이는 0 이상이어야 합니다")
return age # raise가 실행되면 이 줄은 실행되지 않는다
for text in ["17", "-3", "스물"]:
try:
age = check_age(int(text))
except ValueError as e:
print(f"{text}: {e}")
else:
print(f"{text}: 나이 {age}")
# 17: 나이 17
# -3: 나이는 0 이상이어야 합니다
# 스물: invalid literal for int() with base 10: '스물'"-3"은 정수로 변환할 수 있으므로 int("-3")은 오류를 내지 않습니다. 하지만 나이로는 알맞지 않은 값이고, 이것은 파이썬이 아니라 이 프로그램만 판단할 수 있습니다. 그래서 check_age가 raise ValueError(...)로 직접 예외를 발생시킵니다. 자료형은 맞지만 값이 알맞지 않은 경우이므로 ValueError를 골랐습니다.
raise가 실행되면 함수는 그 줄에서 실행을 멈추고 return age는 실행되지 않습니다. 발생한 예외는 함수를 호출한 곳으로 전달되어, 파이썬이 발생시킨 예외와 똑같이 except에서 처리됩니다. 「스물」에서 int가 발생시킨 ValueError와 -3에서 check_age가 발생시킨 ValueError를 except 하나가 함께 처리하는 것도 이 때문입니다. 처리하는 곳이 없으면 프로그램이 멈춥니다.
except ValueError as e:처럼 as 뒤에 변수 이름을 쓰면 발생한 예외가 그 변수에 저장됩니다. 그 변수를 출력하면 raise에 쓴 메시지가 나오고, 파이썬이 발생시킨 예외라면 파이썬이 정한 영어 메시지가 나옵니다.
함수 안에서 출력하지 않고 예외를 발생시키는 이유
check_age가 안내 문구만 출력하고 끝나면, 호출한 곳에서는 값이 잘못됐다는 것을 알 수 없어 다음 줄을 그대로 실행합니다. 예외를 발생시키면 그 상황에서 무엇을 할지를 호출한 곳에서 정할 수 있습니다. 다시 입력받을 수도 있고, 처리하지 않고 멈추게 둘 수도 있습니다.
스스로 확인
while True: 안의 try 문에 finally: print("확인")을 붙였습니다. 올바른 값이 입력되어 try 안의 break로 반복을 빠져나갈 때도 「확인」이 출력될까요?
핵심 정리
멈추는 것은 코드가 틀려서만이 아니다
int("스물")은 잘 작성한 코드가 예상 밖의 값을 만난 것이다. 이런 상황은 없앨 수 없으므로 발생했을 때 무엇을 할지 정해 둔다. 음수 나이처럼 파이썬은 알아채지 못하지만 알맞지 않은 값은 raise로 직접 예외를 발생시킨다.
잡을 수 있는 것은 실행 오류뿐
콜론을 빠뜨린 문법 오류는 실행 전에 발견되므로 try로 잡을 수 없다. 멈추지 않고 결과만 틀린 논리 오류도 예외가 발생하지 않으므로 잡히지 않는다.
갈림길이 생긴다
try 안에서 오류가 나면 except로, 나지 않으면 else로 간다. 어느 쪽으로 갔든 그 뒤의 줄로 이어진다. except에 쓰지 않은 종류의 오류가 나면 그대로 멈춘다. finally 안은 이 세 경우 모두, return으로 빠져나갈 때도 try 문을 벗어나기 직전에 실행된다.
try 안은 짧게
열 줄을 넣으면 어느 줄에서 오류가 났는지 알기 어렵다. 오류가 날 수 있는 줄만 감싼다.
좁게 잡는다
except:는 내 오타로 난 오류까지 삼켜 고칠 곳을 감춘다. 발생했을 때 할 일을 정해 둔 오류만 잡고 나머지는 멈추게 둔다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. try로 잡을 수 없는 것은?
2. try 안에서 오류가 나지 않았을 때만 실행되는 부분은?
3. except:라고만 쓰는 것이 위험한 이유는?