출시·고도화 중
Python 안내서 · 5/6
Python은 오류를 반환값이 아니라 예외(exception)로 알립니다. 이 장에서는 예외를 잡고 다루는 try 문의 네 부분, 직접 예외를 일으키고 만드는 법, 자원을 안전하게 정리하는 컨텍스트 관리자, 그리고 pytest와 unittest로 코드를 검증하는 방법을 살펴봅니다.
예외가 일어나면 실행이 그 자리에서 멈추고, 이를 처리하는 except를 찾을 때까지 호출한 쪽으로 거슬러 올라갑니다. 끝까지 처리되지 않으면 프로그램이 트레이스백(traceback)을 출력하고 종료합니다. try 문은 네 부분으로 이루어집니다.
| 절 | 실행되는 때 |
|---|---|
try | 예외가 날 수 있는 코드를 실행합니다 |
except | 지정한 종류의 예외가 났을 때 |
else | 예외 없이 try가 끝났을 때 |
finally | 예외 여부와 상관없이 항상 마지막에 |
def read_number(path: str) -> int:
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print(f"{path} 파일이 없습니다")
return 0
else:
try:
return int(f.read().strip())
except ValueError as e:
print(f"숫자가 아닙니다: {e}")
return 0
finally:
f.close() # 성공하든 실패하든 파일을 닫습니다except에는 처리할 수 있는 구체적인 예외만 적습니다. 아무 예외나 잡는 맨 except:나 except Exception:을 남발하면 진짜 버그까지 숨겨 버립니다. 여러 예외를 함께 처리할 때는 except (KeyError, IndexError):처럼 튜플로 묶습니다.
잘못된 입력이나 상태를 만나면 raise로 예외를 일으킵니다. 도메인에 맞는 예외가 필요하면 Exception을 상속해 직접 만듭니다. 기반 예외 클래스를 하나 두고 세부 예외를 그 아래에 두면, 호출하는 쪽이 원하는 수준에서 골라 잡을 수 있습니다.
class PaymentError(Exception):
"""결제 관련 오류의 기반 클래스"""
class InsufficientFunds(PaymentError):
def __init__(self, needed: int, available: int) -> None:
super().__init__(f"{needed}원이 필요하지만 {available}원뿐입니다")
self.needed = needed
self.available = available
def pay(balance: int, amount: int) -> int:
if amount <= 0:
raise ValueError("amount must be positive")
if amount > balance:
raise InsufficientFunds(amount, balance)
return balance - amount
try:
pay(1000, 5000)
except PaymentError as e:
print("결제 실패:", e)다른 예외를 잡은 뒤 더 의미 있는 예외로 바꿔 던질 때는 raise NewError(...) from e를 씁니다. 원래 원인이 트레이스백에 함께 남아 디버깅이 쉬워집니다.
파일, 네트워크 연결, 잠금처럼 쓰고 나면 반드시 정리해야 하는 자원은 with 문으로 다룹니다. 블록을 벗어날 때 예외가 났더라도 자원이 자동으로 정리되므로 try/finally를 직접 쓰는 것보다 짧고 안전합니다. 직접 만들 때는 contextlib.contextmanager 데코레이터가 간편합니다.
import time
from contextlib import contextmanager
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("hello\n")
# 여기서 파일은 이미 닫혀 있습니다
@contextmanager
def timer(label: str):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}초")
with timer("합계 계산"):
total = sum(range(1_000_000))pytest는 Python에서 가장 널리 쓰이는 테스트 도구입니다. test_로 시작하는 파일과 함수를 자동으로 찾아 실행하며, 특별한 단언 메서드 없이 평범한 assert만 쓰면 실패했을 때 값을 자세히 보여 줍니다.
# tests/test_pay.py
import pytest
from payments import InsufficientFunds, pay
def test_pay_reduces_balance():
assert pay(1000, 300) == 700
def test_pay_rejects_overdraft():
with pytest.raises(InsufficientFunds):
pay(100, 500)
@pytest.mark.parametrize("amount", [0, -10])
def test_pay_rejects_non_positive(amount):
with pytest.raises(ValueError, match="positive"):
pay(1000, amount)
def test_writes_file(tmp_path):
target = tmp_path / "out.txt" # 테스트마다 새로 만드는 임시 폴더
target.write_text("ok", encoding="utf-8")
assert target.read_text(encoding="utf-8") == "ok"python -m pip install pytest
pytest # 모든 테스트 실행
pytest -k overdraft -v # 이름에 overdraft 가 들어간 테스트만 자세히tmp_path처럼 테스트 함수의 매개변수 이름으로 준비물을 받는 기능을 픽스처(fixture)라고 합니다. @pytest.fixture로 직접 만들 수도 있습니다.
표준 라이브러리에는 unittest도 들어 있어 추가 설치 없이 쓸 수 있습니다. TestCase를 상속한 클래스에 test_로 시작하는 메서드를 두고 assertEqual 같은 메서드로 검사합니다. pytest는 unittest 형식의 테스트도 그대로 실행합니다.
import unittest
from payments import pay
class PayTest(unittest.TestCase):
def test_pay(self):
self.assertEqual(pay(1000, 300), 700)
def test_negative(self):
with self.assertRaises(ValueError):
pay(1000, -1)
if __name__ == "__main__":
unittest.main()try/except/else/finally로 예외를 다루고, 처리할 수 있는 구체적인 예외만 잡습니다.raise로 예외를 일으키고, Exception을 상속해 도메인 예외를 만들며, 원인은 from으로 잇습니다.with 문과 컨텍스트 관리자로 다룹니다.assert, pytest.raises, 픽스처로 간결하게 쓰고, 표준 unittest도 함께 쓸 수 있습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.