오류와 예외
오류와 예외
파이썬을 배우다 보면 에러 메시지를 만나지 않고는 지나갈 수 없어요. 크게 나누면 문법이 틀려서 파싱 자체가 안 되는 '구문 오류'와, 문법은 맞지만 실행 중에 생기는 '예외'가 있어요. 예외는 무조건 프로그램을 멈추는 게 아니라 try/except로 잡아서 우아하게 처리할 수 있어요. 이번에는 예외가 어떻게 발생하고, 어떻게 잡고, 어떻게 다시 던지는지, 그리고 정리 작업을 보장하는 finally·with까지 차근차근 볼게요.
구문 오류 vs 예외
구문 오류(syntax error)는 파싱 단계에서 잡혀요. 아래처럼 콜론이 빠지면 인터프리터가 틀린 위치를 화살표로 알려줘요.
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
예외(exception)는 문장이 문법적으로 맞아도 실행할 때 발생해요. 오류 메시지의 마지막 줄이 무엇이 잘못됐는지 알려주고, 그 앞부분은 스택 트레이스백(traceback)으로 어디서 발생했는지 보여줘요.
10 * (1 / 0) # ZeroDivisionError: division by zero
4 + spam * 3 # NameError: name 'spam' is not defined
'2' + 2 # TypeError: can only concatenate str (not "int") to str
예외는 타입이 있고, 그 타입은 메시지 일부로 함께 출력돼요. 위의 ZeroDivisionError·NameError·TypeError가 각각 내장 예외 이름이에요.
예외 처리하기: try / except
예외를 잡으려면 try 절 안에 위험한 작업을 두고 except 절로 처리해요.
while True:
try:
x = int(input("숫자를 입력하세요: "))
break
except ValueError:
print("올바른 숫자가 아니에요. 다시 입력해 주세요.")
try 문에는 except 절을 여러 개 둘 수 있고, 그중 하나의 핸들러만 실행돼요. 핸들러는 같은 try 안의 다른 핸들러에서 난 예외까지는 잡지 않아요. 여러 예외를 한 번에 받으려면 괄호로 묶어요.
except (RuntimeError, TypeError, NameError):
pass
예외 객체를 변수로 받으면 예외 타입과 인자 정보를 확인할 수 있어요.
try:
raise Exception('spam', 'eggs')
except Exception as inst:
print(type(inst)) # <class 'Exception'>
print(inst.args) # ('spam', 'eggs')
BaseException은 모든 예외의 공통 기반 클래스고, 그 하위 클래스인 Exception이 '치명적이지 않은' 예외들의 기반이에요. SystemExit(프로그램 종료)나 KeyboardInterrupt(사용자 중단)처럼 프로그램이 끝나야 한다는 신호는 Exception의 하위가 아니라서, 보통은 굳이 잡지 않아요. 넓게 잡을 땐 Exception 하나로 거의 다 걸리지만, 가능한 구체적 타입만 잡고 예상 밖의 예외는 위로 흘려보내는 게 좋은 습관이에요.
예외 핸들러는 try 절 안에서 직접 일어난 예외뿐 아니라, 그 안에서 호출된 함수(간접적으로라도)에서 일어난 예외까지도 잡아요.
else와 finally 절
try/except 뒤에는 else 절을 붙일 수 있고, 존재하면 모든 except보다 뒤에 와야 해요. else는 try 절이 예외 없이 정상 종료됐을 때만 실행돼요. 이렇게 하면 원래 보호하려던 코드가 아닌 다른 코드에서 난 예외를 실수로 잡는 일을 피할 수 있어요.
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
finally 절은 예외 발생 여부와 관계없이 항상 마지막에 실행돼요. 정리 작업(자원 해제 등)을 보장할 때 써요.
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
else:
print("result is", result)
finally:
print("executing finally clause")
finally 안에 return/break/continue를 두면 그 값이 우선해 예외가 다시 던져지지 않을 수 있어서 혼란스러우니, 되도록 피하는 게 좋아요.
예외 던지기와 체이닝: raise · from
프로그래머가 직접 예외를 일으키고 싶을 때는 raise 문을 써요. raise의 인자는 예외 인스턴스거나 예외 클래스여야 하고, 클래스를 주면 암묵적으로 인스턴스화돼요.
raise NameError('HiThere')
이미 잡은 예외에서 raise를 인자 없이 다시 쓰면, 그 예외를 그대로 다시 던져요. 그리고 예외를 처리하는 중에 다른 예외가 발생하면 '체이닝'으로 묶여 출력돼요.
try:
func()
except ConnectionError as exc:
raise RuntimeError('Failed to open database') from exc
from None을 쓰면 체이닝 정보를 숨기고 새 예외만 남길 수 있어요. 이렇게 하면 의도적으로 예외를 정리하고 싶을 때 출력을 깔끔하게 유지할 수 있답니다.
사용자 정의 예외
필요하면 직접 예외 클래스를 정의할 수 있어요. 예외는 관례상 Exception에서 (직접이든 간접이든) 파생시키고, 이름 끝을 Error로 붙이는 게 표준과 같아요. 예외 클래스는 보통 단순하게 유지하고, 핸들러가 정보를 추출할 수 있도록 몇 개의 속성만 제공하는 경우가 많아요.
class MyError(Exception):
def __init__(self, message):
self.message = message
super().__init__(message)
정리 작업 보장: with 문
파일처럼 정리 동작을 정의한 객체는 with 문으로 안전하게 쓸 수 있어요. with 블록을 빠져나오면 예외가 있든 없든 항상 파일이 닫혀요. 그냥 open()으로 열면 코드가 끝난 뒤에도 파일이 언제 닫힐지 모르는데, with를 쓰면 그 걱정이 사라져요.
with open('myfile.txt') as f:
for line in f:
print(line)
# 블록을 벗어나면 f는 항상 닫힘
더 알아보기
- 파이썬 클래스 — 사용자 정의 예외를 만들 때 필요한 클래스 문법
- 내장 예외 레퍼런스 — 모든 내장 예외와 의미
- 파이썬 모듈 — 표준 모듈이 던지는 예외