open()과 with 문의 컨텍스트 매니저 프로토콜 원리를 이해하고 pathlib로 현대적 파일 처리를 완전히 마스터한다.
🎯 이 강의에서 배우는 것
파일 처리는 모든 실용 프로그램의 기초입니다. 파이썬의 with 문은 파일을 반드시 닫히게 보장하는 컨텍스트 매니저 프로토콜을 사용합니다. __enter__와 __exit__의 내부 원리를 이해하면 데이터베이스 연결, 락(lock), 임시 디렉토리 등 모든 자원 관리에 같은 패턴을 적용할 수 있습니다. 현대적인 파일 경로 처리인 pathlib도 함께 배웁니다.
📂 open()과 파일 모드 완전 분석
import os
# open(file, mode='r', encoding=None, errors='strict', buffering=-1)
# mode 조합:
# 'r' — 읽기 (기본)
# 'w' — 쓰기 (기존 파일 덮어씀)
# 'a' — 추가 (파일 끝에 추가)
# 'x' — 새 파일 생성 (이미 있으면 FileExistsError)
# 'b' — 바이너리 모드 (rb, wb, ab)
# '+' — 읽기+쓰기 (r+, w+, a+)
# 항상 encoding='utf-8' 명시! 기본값은 플랫폼마다 다름
# Windows: cp949, macOS/Linux: UTF-8
# 쓰기
with open("sample.txt", "w", encoding="utf-8") as f:
f.write("첫 번째 줄\n")
f.write("두 번째 줄\n")
f.writelines(["세 번째\n", "네 번째\n"])
# 읽기 — 전체
with open("sample.txt", "r", encoding="utf-8") as f:
content = f.read() # 전체 문자열
print(repr(content))
# 읽기 — 줄 단위 (대용량 파일에 유리)
with open("sample.txt", "r", encoding="utf-8") as f:
for i, line in enumerate(f, 1): # 파일 객체 자체가 이터러블
print(f"줄 {i}: {line}", end="")
# 위치 제어
with open("sample.txt", "r", encoding="utf-8") as f:
first_line = f.readline()
pos = f.tell() # 현재 위치(바이트)
print(f"첫 줄: {first_line!r}")
print(f"위치: {pos}")
f.seek(0) # 파일 처음으로
all_content = f.read()
# 정리
os.remove("sample.txt")
# 바이너리 모드: 원시 바이트
with open("test.bin", "wb") as f:
f.write(b"\x89PNG\r\n") # PNG 시그니처 (bytes 리터럴)
with open("test.bin", "rb") as f:
header = f.read(4)
print(header) # b'\x89PNG'
os.remove("test.bin")
🔒 with 문 — 컨텍스트 매니저 프로토콜
# with 문의 변환:
# with open("file") as f:
# ...
# ↓ 내부적으로:
# mgr = open("file")
# f = mgr.__enter__()
# try:
# ...
# except:
# if not mgr.__exit__(*sys.exc_info()):
# raise # __exit__가 True 반환하면 예외 억제
# else:
# mgr.__exit__(None, None, None)
# 파일 객체의 __enter__/__exit__ 직접 확인
f = open("temp.txt", "w", encoding="utf-8")
result = f.__enter__() # f 자신을 반환
print(result is f) # True
f.__exit__(None, None, None) # 파일 닫기
import os; os.remove("temp.txt")
# 커스텀 컨텍스트 매니저 구현
class Timer:
import time
def __enter__(self):
import time
self._start = time.perf_counter()
return self # as 변수에 바인딩
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self._start
print(f"경과 시간: {elapsed:.4f}초")
return False # False(또는 None) → 예외 전파, True → 예외 억제
with Timer() as t:
total = sum(range(1_000_000))
# "경과 시간: X.XXXXs" 출력
# 예외 처리 in __exit__
class SuppressZeroDivision:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ZeroDivisionError:
print(f"ZeroDivisionError 억제: {exc_val}")
return True # 예외 억제 (프로그램 계속)
return False
with SuppressZeroDivision():
x = 1 / 0 # 오류 억제됨!
print("이 줄은 실행 안 됨") # __exit__가 호출될 때 블록 탈출
print("계속 실행") # 실행됨
🎁 contextlib — 제너레이터 기반 컨텍스트 매니저
from contextlib import contextmanager, suppress
import os, tempfile
# @contextmanager: 클래스 없이 컨텍스트 매니저 구현
@contextmanager
def managed_file(path: str, mode: str = "w", encoding: str = "utf-8"):
# yield 이전: __enter__ 역할
f = open(path, mode, encoding=encoding)
try:
yield f # as 변수에 f 바인딩
except Exception:
f.close()
raise # 예외 전파
else:
f.close()
with managed_file("test.txt") as f:
f.write("컨텍스트 매니저 테스트")
os.remove("test.txt")
# @contextmanager 임시 디렉토리
@contextmanager
def temp_directory():
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir # 임시 경로 반환
finally:
import shutil
shutil.rmtree(tmpdir) # with 블록 탈출 시 자동 삭제
with temp_directory() as tmpdir:
print(f"임시 디렉토리: {tmpdir}")
# 이 블록 안에서 파일 생성 — 블록 탈출 후 자동 삭제
# contextlib.suppress: 특정 예외 억제
with suppress(FileNotFoundError):
os.remove("없는파일.txt") # FileNotFoundError 무시
print("suppress 이후 계속")
# 여러 컨텍스트 매니저 동시 사용
with open("a.txt", "w") as fa, open("b.txt", "w") as fb:
fa.write("A파일")
fb.write("B파일")
for name in ["a.txt", "b.txt"]:
os.remove(name)
🗂️ pathlib.Path — 현대적 파일 경로 처리
from pathlib import Path
# Path: 운영체제에 상관없이 동작
home = Path.home() # 홈 디렉토리
cwd = Path.cwd() # 현재 작업 디렉토리
script_dir = Path(__file__).parent # 현재 스크립트 디렉토리
# 경로 조합 (/ 연산자 오버로딩)
config = home / ".config" / "myapp" / "settings.json"
print(config) # /Users/name/.config/myapp/settings.json
# 경로 속성
p = Path("/home/user/project/main.py")
print(p.name) # main.py
print(p.stem) # main (확장자 제외)
print(p.suffix) # .py
print(p.parent) # /home/user/project
print(p.parts) # ('/', 'home', 'user', 'project', 'main.py')
# 파일 읽기/쓰기 — 한 줄로
text_file = Path("hello.txt")
text_file.write_text("안녕하세요!\n두 번째 줄", encoding="utf-8")
content = text_file.read_text(encoding="utf-8")
print(content)
# 바이너리
bin_file = Path("data.bin")
bin_file.write_bytes(b"\x00\x01\x02\x03")
data = bin_file.read_bytes()
print(data)
# 존재 확인
print(text_file.exists()) # True
print(text_file.is_file()) # True
print(text_file.is_dir()) # False
# 파일 탐색
# current_dir = Path(".")
# for py_file in current_dir.glob("*.py"): # 현재 폴더의 .py 파일
# print(py_file)
# for py_file in current_dir.rglob("*.py"): # 하위 폴더 포함
# 정리
text_file.unlink() # 파일 삭제 (os.remove 대체)
bin_file.unlink()
⚠️ 자주 하는 실수
- encoding 생략:
open("file.txt")의 기본 인코딩은 플랫폼마다 다릅니다. Windows는 보통 cp949, macOS/Linux는 UTF-8. 항상encoding="utf-8"을 명시하세요. 특히 한글 파일을 다룰 때 필수입니다. - with 없이 파일 열기:
f = open(...)으로 열고 예외가 발생하면 파일이 닫히지 않습니다. 항상with open(...) as f:를 사용하세요. OS의 파일 디스크립터가 고갈될 수 있습니다. - 대용량 파일을 한 번에 read(): 1GB 파일에
f.read()를 하면 메모리에 모두 올라옵니다. 대용량 파일은for line in f:로 한 줄씩 처리하거나f.read(chunk_size)로 청크 단위로 처리하세요.
📝 정리 및 다음 강의 예고
open(): 항상encoding="utf-8"명시, 항상with로 열기.- 컨텍스트 매니저:
__enter__/__exit__프로토콜.with가 예외에도 항상__exit__호출 보장. @contextmanager: 제너레이터로 컨텍스트 매니저 간단 구현. yield 전이 enter, 후가 exit.pathlib.Path: 현대적 경로 처리./로 경로 조합,read_text/write_text로 한 줄 파일 처리.
다음 강의: 마지막 강의 — JSON/CSV 처리, 모듈·패키지·가상환경. 파이썬 생태계를 완성하고 실전 프로젝트를 위한 구조 설계를 배웁니다.
관련 주제
- open() 파일모드
- with문 컨텍스트매니저
- __enter__·__exit__
- contextlib.contextmanager
- pathlib.Path
- 텍스트 vs 바이너리 모드
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 40강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
