16. 파일 입출력과 데이터 형식
open과with로 파일을 안전하게 읽고 쓴다.- 읽기/쓰기/추가 모드와 줄 단위 처리를 안다.
- CSV 파일을
csv모듈로 다룬다. - JSON으로 데이터를 저장하고 불러온다.
먼저: 파일 입출력이 왜 필요한가
지금까지 만든 프로그램은 종료되면 모든 데이터가 사라졌습니다. 변수는 메모리에만 있으니까요. 파일에 저장하면 프로그램을 껐다 켜도 데이터가 남습니다. 이를 영속성(persistence)이라 하고, 그 첫걸음이 파일 입출력입니다.
with로 파일 열기
파일을 다루는 기본 도구는 open() 함수입니다. 그런데 파일은 다 쓰면 반드시 닫아야 합니다(close()). 닫지 않으면 데이터가 제대로 저장 안 되거나 자원이 새어 나갈 수 있습니다.
with 구문을 쓰면 블록이 끝날 때 파일이 자동으로 닫힙니다. 그래서 항상 with로 파일을 엽니다.
with open("memo.txt", "w", encoding="utf-8") as f: f.write("첫째 줄\n") f.write("둘째 줄\n") # 여기서 자동으로 close() 됨
open()의 인자를 봅시다.
- 첫째: 파일 이름(경로)
- 둘째: 모드(
"w"쓰기,"r"읽기,"a"추가) encoding="utf-8": 한글이 깨지지 않게 하는 인코딩. 한글을 다룰 땐 항상 지정하세요.
with가 실제로 대신해주는 일
with가 왜 편한지는, 그것 없이 직접 닫아야 하는 코드와 비교하면 분명합니다.
# with 없이: 직접 열고 닫기 (번거롭고 위험) f = open("memo.txt", "w", encoding="utf-8") try: f.write("내용") finally: f.close() # 에러가 나도 반드시 닫아야 하니 finally에 # with 사용: 위 전체와 동등 (닫기를 자동으로!) with open("memo.txt", "w", encoding="utf-8") as f: f.write("내용") # 블록을 벗어나면 (에러가 나든 안 나든) 자동으로 close()
위 두 코드는 완전히 같은 일을 합니다. with는 "블록을 벗어날 때 close()를 자동 호출"해주는 장치라서, 우리가 try/finally로 직접 닫을 필요가 없습니다. 닫기를 깜빡할 위험도 사라지죠.
with open(...) as f:로 연다. 블록을 벗어나면 자동으로 닫혀 안전하다. 한글이 있으면 encoding="utf-8"을 잊지 말 것.📎with는 14장에서 본 "자원을 안전하게 정리"하는 구문입니다. 파일 외에도 네트워크 연결, 잠금 등 "열었으면 닫아야 하는" 자원에 두루 쓰입니다. 초급편 예외 처리의finally가 하던 "마무리"를 더 깔끔하게 해주는 도구죠.
파일 쓰기
쓰기 모드("w")로 열면 파일에 내용을 씁니다.
with open("memo.txt", "w", encoding="utf-8") as f: f.write("안녕하세요\n") # 한 문자열 쓰기 (\n으로 줄바꿈) f.write("두 번째 줄\n") f.writelines(["셋째\n", "넷째\n"]) # 여러 줄을 리스트로
"w" 모드는 기존 파일 내용을 전부 지우고 새로 씁니다. 기존 내용에 이어 쓰려면 "a"(append) 모드를 쓰세요.```python
with open("log.txt", "a", encoding="utf-8") as f: # 기존 내용 뒤에 추가
f.write("새 로그\n")
```
write()는 print()와 달리 줄바꿈을 자동으로 넣지 않습니다. 줄을 나누려면 \n을 직접 붙여야 합니다.| 모드 | 의미 | 파일이 없으면 | 기존 내용 |
|---|---|---|---|
"r" | 읽기 | 에러 | 보존 |
"w" | 쓰기 | 새로 생성 | 전부 삭제 |
"a" | 추가 | 새로 생성 | 보존(뒤에 덧붙임) |
파일 읽기
읽기 모드("r", 생략 가능)로 엽니다. 읽는 방법은 여러 가지입니다.
전체를 한 번에: read()
with open("memo.txt", "r", encoding="utf-8") as f: content = f.read() # 파일 전체를 하나의 문자열로 print(content)
⭐ 줄 단위로 순회 (권장)
파일 객체를 for로 돌면 한 줄씩 나옵니다. 큰 파일도 메모리 효율적입니다(14장의 지연 평가와 같은 원리).
with open("memo.txt", "r", encoding="utf-8") as f: for line in f: print(line.strip()) # strip()으로 줄 끝 \n 제거
\n이 붙어 있어, print하면 빈 줄이 끼어 보입니다. line.strip()으로 양끝 공백·줄바꿈을 제거하면 깔끔합니다.모든 줄을 리스트로: readlines()
with open("memo.txt", encoding="utf-8") as f: lines = f.readlines() # ['첫째 줄\n', '둘째 줄\n', ...] print(len(lines)) # 줄 개수
| 방법 | 결과 | 적합한 경우 |
|---|---|---|
f.read() | 전체를 한 문자열로 | 작은 파일, 통째로 처리 |
for line in f: | 한 줄씩 (지연) | 큰 파일, 줄별 처리 |
f.readlines() | 모든 줄을 리스트로 | 줄 목록이 필요할 때 |
"r"로 열면 FileNotFoundError가 납니다. 초급편의 try/except로 대비하세요.```python
try:
with open("없는파일.txt", encoding="utf-8") as f:
data = f.read()
except FileNotFoundError:
print("파일이 없습니다")
```
CSV: 표 형태 데이터
CSV(Comma-Separated Values)는 값을 쉼표로 구분해 표처럼 저장하는 형식입니다. 엑셀로 열리는 그 형식이죠. csv 모듈로 다룹니다.
CSV 쓰기
import csv with open("people.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["이름", "나이", "도시"]) # 헤더 writer.writerow(["민지", 25, "서울"]) writer.writerow(["현우", 30, "부산"])
newline="": CSV를 쓸 때는 open에 newline=""을 넣으세요. 빠뜨리면 (특히 Windows에서) 줄 사이에 빈 줄이 끼는 문제가 생깁니다. CSV 작성의 정해진 관용구입니다.CSV 읽기
import csv with open("people.csv", encoding="utf-8") as f: reader = csv.reader(f) for row in reader: print(row) # ['이름', '나이', '도시'] # ['민지', '25', '서울'] # ['현우', '30', '부산']
25가 아니라 '25'로 옵니다. 숫자로 계산하려면 int()/float()로 변환해야 합니다(초급편 형 변환을 떠올리세요).DictReader / DictWriter: 헤더를 키로
csv.DictReader는 각 행을 딕셔너리로 읽어, 열 이름(헤더)으로 값에 접근하게 해줍니다. 위치(인덱스)보다 이름이 명확하죠.
import csv with open("people.csv", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: print(row["이름"], row["나이"]) # 헤더 이름으로 접근 # 민지 25 # 현우 30
# DictWriter로 쓰기 with open("out.csv", "w", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=["name", "score"]) writer.writeheader() # 헤더 줄 쓰기 writer.writerow({"name": "Alice", "score": 90}) writer.writerow({"name": "Bob", "score": 85})
JSON: 구조화된 데이터
JSON(JavaScript Object Notation)은 데이터를 {"키": 값} 형태로 적는, 사람도 컴퓨터도 읽기 좋은 형식입니다. 웹 API·설정 파일에서 표준처럼 쓰입니다. 모양이 Python 딕셔너리와 거의 같아 다루기 쉽습니다.
json 모듈의 핵심은 네 함수입니다.
| 함수 | 방향 | 대상 |
|---|---|---|
json.dump(obj, f) | Python → 파일 | 객체를 파일에 저장 |
json.load(f) | 파일 → Python | 파일에서 객체로 불러옴 |
json.dumps(obj) | Python → 문자열 | 객체를 JSON 문자열로 |
json.loads(s) | 문자열 → Python | JSON 문자열을 객체로 |
s가 붙으면 "string". dump/load는 파일 상대, dumps/loads는 문자열 상대입니다.JSON 저장하기
import json data = { "name": "민지", "age": 25, "hobbies": ["독서", "코딩"], "active": True, } with open("data.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)
저장된 파일 내용:
{
"name": "민지",
"age": 25,
"hobbies": [
"독서",
"코딩"
],
"active": true
}
ensure_ascii=False: 이게 없으면 한글이\uBBFC\uC9C0처럼 깨져 보입니다. 한글엔 필수.indent=2: 들여쓰기로 보기 좋게. 없으면 한 줄로 빽빽하게 저장됩니다.
True가 JSON에서는 소문자 true가 됩니다. None은 null이 되고요. JSON과 Python의 표기가 조금 다르지만, 변환은 json 모듈이 알아서 해줍니다.JSON 불러오기
import json with open("data.json", encoding="utf-8") as f: loaded = json.load(f) # 다시 Python 딕셔너리로 print(loaded["name"]) # 민지 print(loaded["hobbies"]) # ['독서', '코딩']
문자열과의 변환
파일이 아니라 문자열로 주고받을 때는 dumps/loads를 씁니다(웹 통신에서 흔함).
import json s = json.dumps({"x": 1, "y": 2}, ensure_ascii=False) print(s) # {"x": 1, "y": 2} (문자열) obj = json.loads('{"a": 10, "b": [1, 2, 3]}') print(obj["b"]) # [1, 2, 3] (딕셔너리로)
CSV vs JSON, 언제 무엇을?
flowchart TD
Q{"데이터 모양은?"}:::proc
Q -->|"표(행과 열, 균일)"| CSV["📊 CSV<br/>스프레드시트·표 데이터<br/>엑셀 호환"]:::result
Q -->|"중첩·계층 구조"| JSON["🗂️ JSON<br/>설정·API·복잡한 구조<br/>리스트 안에 딕셔너리 등"]:::result
classDef proc fill:#fff3b0,stroke:#e0a800,color:#5c4500
classDef result fill:#b8e6c1,stroke:#34a853,color:#14532d
- CSV: 모든 행이 같은 열을 가진 표 형태. 가계부, 성적표, 통계 데이터.
- JSON: 중첩·계층이 있는 데이터. 설정 파일, 사용자 프로필(취미 리스트 등 안에 또 구조가 있는 경우).
이 장에서 배운 것
- 파일은 항상
with open(...) as f:로 연다(자동으로 닫힘). 한글엔encoding="utf-8". - 모드:
"r"(읽기)·"w"(덮어쓰기)·"a"(추가).write()는 줄바꿈을 자동으로 안 넣는다. - 읽기는
read()(전체)·for line in f(줄 단위, 권장)·readlines()(리스트). 줄 끝\n은strip()으로 제거. - CSV는
csv모듈로 다루며, 쓸 때newline=""을 넣고, 읽은 값은 전부 문자열이다.DictReader로 헤더 이름 접근. - JSON은
dump/load(파일)·dumps/loads(문자열). 한글엔ensure_ascii=False, 보기 좋게indent=2.
🧪 실습 문제
문제 1. diary.txt 파일에 세 줄("월요일", "화요일", "수요일")을 쓰는 코드를 작성하세요. (각 줄이 실제로 나뉘도록)
문제 2. 문제 1의 파일을 읽어, 각 줄 앞에 번호를 붙여 1. 월요일 형태로 출력하세요. (힌트: enumerate, strip)
문제 3. 다음 코드의 문제점은 무엇이고 어떻게 고쳐야 할까요?
with open("data.txt", "w") as f: f.write("기존 중요 데이터") # (나중에) with open("data.txt", "w") as f: f.write("새 데이터")
문제 4. 딕셔너리 config = {"theme": "dark", "volume": 70, "tags": ["a", "b"]}를 config.json에 한글 깨짐 없이, 들여쓰기 2칸으로 저장하는 코드를 쓰세요.
문제 5. 다음 JSON 문자열을 Python 객체로 변환하고, scores 리스트의 합을 출력하세요.
text = '{"name": "반1", "scores": [80, 90, 75]}'
<details>
<summary>✅ 정답·해설 보기</summary>
1.
with open("diary.txt", "w", encoding="utf-8") as f: f.write("월요일\n") f.write("화요일\n") f.write("수요일\n") # 또는: f.writelines([f"{d}\n" for d in ["월요일","화요일","수요일"]])
2.
with open("diary.txt", encoding="utf-8") as f: for i, line in enumerate(f, start=1): print(f"{i}. {line.strip()}")
3. 두 번째 "w" 모드가 기존 파일을 덮어써 "기존 중요 데이터"가 사라집니다. 이어 쓰려면 두 번째를 "a"(추가) 모드로 열어야 합니다.
4.
import json config = {"theme": "dark", "volume": 70, "tags": ["a", "b"]} with open("config.json", "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2)
5.
import json text = '{"name": "반1", "scores": [80, 90, 75]}' obj = json.loads(text) print(sum(obj["scores"])) # 245
</details>
◀️ 이전 장: 15. 모듈과 패키지 | ▶️ 다음 장: 17. 데코레이터