파일과 예외
실제로 의미 있는 일을 하는 대부분의 프로그램은 파일시스템에 접근합니다. 설정을 읽고, 결과를 쓰고, 데이터를 불러옵니다. 그리고 문제가 발생하면 Python은 **예외(exception)**를 발생시킵니다. 예상치 못한 일이 일어났음을 알리는 신호입니다. 이 장에서는 두 가지를 다룹니다. 파일에서 데이터를 읽고 쓰는 방법, 그리고 충돌하지 않고 오류를 우아하게 처리하는 코드를 작성하는 방법입니다.
파일 열기
open()은 파일을 열고, 그로부터 읽거나 쓸 수 있는 객체를 반환합니다. 경로와 파일로 무엇을 할지(읽기, 쓰기, 추가)를 지정합니다. 사용을 마치면 반드시 파일을 닫아야 하는데, with 문이 이를 자동으로 처리해줍니다.
f = open("data.txt", "r") # "r" = 읽기
content = f.read()
f.close()"r"은 **모드(mode)**입니다.
| 모드 | 의미 |
|---|---|
"r" | 읽기. 파일이 존재해야 함. 기본 모드. |
"w" | 쓰기. 파일을 새로 만들거나 덮어씀. |
"a" | 추가. 기존 내용을 지우지 않고 끝에 덧붙임. |
"x" | 생성. 파일이 이미 존재하면 실패. |
"r+" | 읽기 및 쓰기. |
"b" | 바이너리. 다른 모드에 결합: "rb", "wb". |
사용을 마치면 반드시 .close()를 호출하세요. 잊어버리면 파일이 잠긴 채로 남게 되고 데이터 손상을 일으킬 수 있습니다. 이를 안전하게 처리하는 방법이 바로 with 문입니다.
with 문
with open(...)는 파일을 자동으로 관리해주며, 들여쓰기 블록이 끝나면(혹은 오류가 발생해도) 알아서 닫아줍니다. 수동으로 open()/close()를 쓰는 대신 항상 with open(...)을 사용하세요. 더 안전하고 표준적인 방식입니다.
with open("data.txt", "r") as f:
content = f.read()
# 여기서 f는 보장된 방식으로 닫힘with는 무엇을 하는가?
with는 Python의 컨텍스트 매니저 문법입니다. 설정과 정리 코드를 대신 호출해주며, 여기서는 파일을 열고 안전하게 닫는 역할을 합니다. 내부 동작을 자세히 알 필요는 없습니다. 그냥 open()과 함께 사용하세요.
파일 읽기
읽기에는 세 가지 방법이 있습니다. .read()는 파일 전체를 하나의 문자열로 불러옵니다. .readline()은 한 줄을 읽습니다. 파일 객체를 직접 순회하면 한 줄씩 읽는데, 이는 모든 내용을 메모리에 한꺼번에 올리지 않으므로 큰 파일을 다룰 때 가장 효율적입니다.
with open("data.txt", "r") as f:
content = f.read() # 파일 전체를 하나의 문자열로
with open("data.txt", "r") as f:
first_line = f.readline() # 한 줄씩
with open("data.txt", "r") as f:
lines = f.readlines() # 각 줄이 "\n"으로 끝나는 리스트큰 파일의 경우, 한 번에 모두 읽는 것보다 한 줄씩 읽는 것이 더 효율적입니다.
with open("big_file.txt", "r") as f:
for line in f: # 파일을 직접 순회, 메모리 효율적
print(line.strip()) # strip()은 끝의 줄바꿈을 제거파일 객체를 직접 순회하는 것(for line in f)이 큰 파일을 읽는 가장 효율적이고 관용적인 방법입니다.
파일 쓰기
"w" 모드는 파일이 존재하면 전체를 덮어씁니다. "a" 모드는 끝에 추가합니다. .write()는 자동으로 줄바꿈을 추가하지 않으므로, 각 줄의 끝에 "\n"을 명시적으로 포함해야 합니다. 여러 줄을 한꺼번에 쓰려면 "\n".join()으로 합치세요.
with open("output.txt", "w") as f:
f.write("Hello, world\n")
with open("output.txt", "a") as f:
f.write("Another line\n")"w"는 파일이 존재하면 전체를 덮어씁니다. "a"는 끝에 추가합니다.
f.write()는 자동으로 줄바꿈을 추가하지 않으므로 "\n"을 명시적으로 넣어야 합니다. 여러 줄을 한 번에 쓰려면:
lines = ["Line one", "Line two", "Line three"]
with open("output.txt", "w") as f:
f.write("\n".join(lines) + "\n")예외
Python이 처리할 수 없는 문제를 만나면 예외를 발생시킵니다. 무엇이, 어디서 잘못되었는지 설명하는 오류입니다. 처리하지 않으면 프로그램이 충돌하고 트레이스백(traceback)을 출력합니다. 아래 표는 자주 마주하게 되는 예외들입니다.
자주 마주하는 예외들:
| 예외 | 발생 시점 |
|---|---|
FileNotFoundError | open()이 파일을 찾지 못함 |
ValueError | 함수가 올바른 타입이지만 잘못된 내용의 값을 받음 (예: int("abc")) |
TypeError | 타입 자체가 틀림 (예: "hello" + 5) |
KeyError | 딕셔너리 키가 존재하지 않음 |
IndexError | 리스트 인덱스가 범위를 벗어남 |
ZeroDivisionError | 0으로 나눔 |
AttributeError | 객체에 해당 속성이나 메서드가 없음 |
try / except
실패 가능성이 있는 코드를 try 블록으로 감싸세요. 예외가 발생하면 일치하는 except 블록이 충돌 대신 그것을 처리합니다. 어떤 예외를 잡을지 구체적으로 명시하세요. except:로 모든 것을 잡으면 진짜 버그가 가려집니다.
try:
value = int("abc")
except ValueError:
print("That's not a valid number")어떤 예외를 잡을지 구체적으로 지정하세요. except:로 모두 잡으면 버그가 숨겨집니다.
# 나쁨, 프로그래머의 실수까지 모두 잡음
try:
result = do_something()
except:
pass
# 좋음, 예상하고 실제로 처리할 수 있는 것만 잡음
try:
result = do_something()
except FileNotFoundError:
print("File not found")여러 예외 잡기
서로 다른 오류 타입을 별도의 except 블록으로 처리할 수도 있고, 튜플을 사용해 한 블록에서 여러 타입을 잡을 수도 있습니다. as e 부분은 오류 메시지에 접근할 수 있게 해줍니다.
try:
data = int(user_input)
result = 100 / data
except ValueError:
print("Not a number")
except ZeroDivisionError:
print("Can't divide by zero")또는 튜플로 여러 개를 한 번에 잡기:
except (ValueError, ZeroDivisionError) as e:
print(f"Input error: {e}")as e는 예외 객체를 이름에 바인딩하여 메시지를 살펴볼 수 있게 해줍니다.
else와 finally
else는 예외가 발생하지 않은 경우에만 실행됩니다. finally는 예외 발생 여부와 관계없이 항상 실행됩니다. finally는 무슨 일이 있어도 반드시 일어나야 하는 정리 작업에 유용합니다.
try:
with open("data.txt") as f:
content = f.read()
except FileNotFoundError:
print("File not found, using defaults")
content = ""
else:
print("File loaded successfully")
finally:
print("Done attempting to load file") # 항상 실행finally는 파일에 이미 with를 사용하고 있더라도, 연결 닫기나 잠금 해제 같은 정리 작업에 가장 유용합니다.
raise
raise를 사용해 예외를 직접 발생시킬 수도 있습니다. 이는 잘못된 값을 조용히 반환하는 대신, 함수가 호출자에게 문제를 명확히 알릴 수 있게 해줍니다.
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b이렇게 하면 함수가 기대하는 것이 명시적이 되고, 문제를 호출자에게 명확히 알릴 수 있습니다.
커스텀 예외 클래스
더 큰 프로그램에서는 Exception을 상속하여 자체 예외 타입을 정의할 수 있습니다. 이렇게 하면 호출자가 다른 종류의 오류와 분리하여 특정 오류를 잡을 수 있습니다.
class InsufficientFundsError(Exception):
pass
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}, balance is {self.balance}"
)
self.balance -= amounttry:
account.withdraw(1000)
except InsufficientFundsError as e:
print(f"Transaction declined: {e}")JSON
JSON은 모든 것이 사용하는 포맷입니다. API, 설정 파일, 데이터 내보내기 등에 쓰입니다. Python의 json 모듈이 이를 바로 처리해줍니다. json.load()는 파일에서 JSON을 읽어 Python의 dict나 list로 만듭니다. json.dump()는 Python의 dict나 list를 JSON으로 파일에 씁니다.
파일에서 JSON 읽기:
import json
with open("config.json", "r") as f:
config = json.load(f) # JSON을 Python dict/list로 파싱
print(config["setting"])파일에 JSON 쓰기:
import json
data = {"name": "민준", "score": 87, "active": True}
with open("output.json", "w") as f:
json.dump(data, f, indent=2) # indent=는 사람이 읽기 쉽게 만듦JSON과 Python 타입의 대응 관계:
| JSON | Python |
|---|---|
object {} | dict |
array [] | list |
string "" | str |
| number | int 또는 float |
true / false | True / False |
null | None |
파일을 거치지 않고 JSON 문자열과 Python 객체 사이를 변환하려면:
import json
# 문자열에서 Python으로
data = json.loads('{"name": "민준", "score": 87}')
# Python에서 문자열로
text = json.dumps({"name": "민준", "score": 87}, indent=2)json.load()는 파일 객체에서 읽습니다. json.loads()("s"가 붙은 쪽)는 문자열에서 읽습니다.
실전 적용
간단한 게임을 위한 저장/불러오기 패턴입니다. 상태를 JSON에 쓰고, 다음 실행 시 다시 불러오며, 아직 저장 파일이 없으면 기본값으로 폴백합니다:
import json
SAVE_FILE = "save_game.json"
def save_game(player_data: dict) -> None:
with open(SAVE_FILE, "w") as f:
json.dump(player_data, f, indent=2)
print("Game saved.")
def load_game() -> dict:
try:
with open(SAVE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
print("No save file found, starting fresh.")
return {"name": "Player", "score": 0, "level": 1}
state = load_game()
state["score"] += 50
save_game(state)
