파이썬 공부 Day 5: 예외처리 · 파일 입출력 · QR코드오류를 잡고, 파일을 열고, 내 사이트 QR까지 만들어 봤습니다
C에서 오류 처리는 반환값 확인이었습니다. fopen이 NULL을 돌려주면 if로 직접 막아야 했습니다. 파이썬은 오류가 나면 예외가 발생하고, 그걸 받아 내는 문법이 따로 있습니다.
Day 5에는 그 문법(try · except)과 파일 읽고 쓰기를 배웠습니다. 끝에는 둘을 섞어서 QR코드를 여러 장 자동으로 만들었습니다. Day 4에서 배운 함수와 for 문이 계속 같이 나옵니다.
🛡️ try · except: 오류가 나도 꺼지지 않게
0으로 나누는 한 줄이면 프로그램은 그 자리에서 멈춥니다.
# 의도한 오류
# ZeroDivisionError: division by zero
print(10 / 0)
try에는 위험한 코드를, except에는 오류가 났을 때 할 일을 적습니다. 핵심은 except 뒤에 예외 이름을 적는 것입니다.
try:
result = 10 / 0
print(result)
except ZeroDivisionError:
print('0으로 나눌 수 없습니다')
# 0으로 나눌 수 없습니다
이름 없이 except:만 쓰면 모든 오류를 다 삼킵니다. 진짜 버그까지 조용히 숨겨지니, 예외 이름을 쓰는 습관부터 들이기로 했습니다.
헷갈렸던 부분: 콜론 위치
처음에 except: ZeroDivisionError라고 썼다가 엉뚱한 오류를 만났습니다.
# 의도한 오류: 실행하면
# IndentationError: unexpected indent
try:
print(10 / 0)
except: ZeroDivisionError
print('0으로 나눌 수 없습니다')
콜론이 앞에 오면 파이썬은 except:(전부 잡기)와 같은 줄의 ZeroDivisionError라는 문장 하나로 읽습니다. except 블록이 그 줄에서 끝났으니 다음 줄 들여쓰기는 "갑자기 웬 들여쓰기?"가 됩니다. 콜론은 예외 이름 뒤에 붙습니다.
예외 종류별로 나누기
한 try 안에서 서로 다른 오류가 날 수 있습니다. except를 종류별로 나누면 어디가 문제인지 정확히 안내할 수 있습니다.
for value in ['5', '0', 'python']:
try:
# 'python'이면 ValueError
number = int(value)
# 0이면 ZeroDivisionError
result = 50 / number
print(value, '→ 계산 결과:', result)
except ValueError:
print(value, '→ 숫자를 입력하세요.')
except ZeroDivisionError:
print(value, '→ 0을 입력할 수 없습니다.')
# 5 → 계산 결과: 10.0
# 0 → 0을 입력할 수 없습니다.
# python → 숫자를 입력하세요.
except 예외 as e:로 쓰면 e에 발생한 예외가 담기고, print(e)로 오류 메시지를 볼 수 있습니다. 오류를 숨기기만 하지 않고 원인을 남길 수 있습니다.
try:
int('hello')
except ValueError as e:
print(e)
# 출력(실제로는 한 줄)
# invalid literal for int() with base 10:
# 'hello'
자주 만나는 예외
| 예외 | 언제 (예시) |
|---|---|
ValueError | 값이 잘못됨: int('hello') |
TypeError | 타입이 안 맞음: 'hello' + 5 |
ZeroDivisionError | 0으로 나눔: 10 / 0 |
IndexError | 리스트 인덱스 범위 초과: [1, 2, 3][10] |
KeyError | 딕셔너리에 없는 키: {'a': 1}['b'] |
FileNotFoundError | 없는 파일 열기: open('없는파일.txt') |
TypeError와 ValueError는 처음에 구분이 안 됐습니다. TypeError는 자료형 자체가 안 맞을 때입니다('가나' + 5). ValueError는 자료형은 맞는데 값이 이상할 때입니다. int()에 문자열을 넣는 건 괜찮지만 'hi'는 숫자로 바꿀 수 없으니 int('hi')는 ValueError입니다.
else · finally, 그리고 raise
| 블록 | 언제 실행 |
|---|---|
try | 일단 실행 |
except | try에서 오류가 났을 때 |
else | 오류가 안 났을 때만 (없어도 되지만 읽기 편함) |
finally | 오류가 나든 안 나든 무조건 |
def convert(text):
try:
num = int(text)
except ValueError:
print('숫자로 변환할 수 없습니다.')
else:
print('변환 성공 :', num)
finally:
print('예외 처리 구문 종료')
convert('123')
# 변환 성공 : 123
# 예외 처리 구문 종료
convert('aaa')
# 숫자로 변환할 수 없습니다.
# 예외 처리 구문 종료
finally는 파일 닫기나 연결 종료 같은 정리 작업에 자주 씁니다. C에서 goto cleanup으로 모아 두던 정리 코드를 언어가 보장해 주는 자리라고 이해했습니다. 파일은 뒤에 나올 with 문이 이 일을 대신합니다.
raise: 예외를 직접 일으키기
-18은 문법적으로 멀쩡한 정수입니다. 하지만 나이로는 말이 안 됩니다. 이렇게 논리적으로 틀린 값은 raise 예외종류('메시지')로 직접 막습니다.
def check_age(age):
if age < 0:
raise ValueError(
'나이는 0보다 작을 수 없습니다.')
return f'{age}세 확인 완료'
try:
print(check_age(20))
print(check_age(-18))
except ValueError as e:
print(e)
# 20세 확인 완료
# 나이는 0보다 작을 수 없습니다.
예외처리는 함수 안에 넣기
실무에서는 예외처리를 함수 안에 넣어 재사용한다고 합니다. 호출하는 쪽은 try를 신경 쓰지 않아도 됩니다.
def safe_divide(x, y):
try:
return x / y
except ZeroDivisionError:
return '0으로 나눌 수 없습니다'
except TypeError:
return '숫자끼리만 나눌 수 있습니다'
print(safe_divide(10, 5))
# 2.0
print(safe_divide(10, 0))
# 0으로 나눌 수 없습니다
print(safe_divide(10, 'AAA'))
# 숫자끼리만 나눌 수 있습니다
실패하면 None을 돌려주는 변환 함수도 같은 모양입니다.
def parse_int(text):
try:
return int(text)
except ValueError:
return None
for text in ['1000', '3.14', 'hello']:
print(text, '→', parse_int(text))
# 1000 → 1000
# 3.14 → None
# hello → None
'3.14'에서 None이 나와 잠깐 멈칫했습니다. 소수 문자열은 int()로 바로 못 바꾸고 float()을 먼저 거쳐야 합니다.
예외처리를 쓸 때 스스로 정한 규칙은 이렇습니다.
except:만 습관처럼 쓰지 않기. 실제 버그를 놓칩니다- try 범위는 위험한 부분만 좁게
- 오류를 숨기기만 하지 말고
print(e)처럼 원인을 알 수 있는 메시지 남기기
연습문제에서 걸린 것들
문제 1은 인덱스를 입력받아 리스트 원소를 출력하는 문제였습니다. 숫자가 아닌 입력은 ValueError, 범위 밖 인덱스는 IndexError로 따로 잡습니다.
items = ['사과', '바나나', '포도']
try:
index = int(input('인덱스를 입력하세요: '))
print(items[index])
except ValueError:
print('인덱스는 정수로 입력하세요')
except IndexError:
print('인덱스가 범위를 벗어났습니다')
문제 2는 TypeError와 ValueError를 한 번에 보여 줍니다. 문자열과 정수를 더하면 TypeError가 납니다. 정수 자리에 글자를 넣으면 그 전에 int()에서 ValueError가 먼저 납니다.
try:
text = input('문자열을 입력하세요: ')
num = int(input('정수를 입력하세요: '))
# str + int라서 TypeError
print(text + num)
except TypeError:
print('문자열과 숫자는 바로 더할 수 없습니다')
except ValueError:
# 정수 자리에 글자를 넣었을 때
print('정수를 입력하세요')
문제 3에서는 처음에 int(values)로 리스트를 통째로 바꾸려 했습니다. 리스트는 for로 하나씩 꺼내서 try 안에서 변환해야 합니다.
values = ['10', '20', 'python', '30']
for value in values:
try:
print(int(value))
except ValueError:
print('오류')
# 10
# 20
# 오류
# 30
가장 오래 붙잡고 있던 건 실습의 divide 함수였습니다. 두 값이 숫자인지 확인하려고 처음엔 if num1 and num2 is int:라고 썼습니다.
num1, num2 = 10, 2
print(num1 and num2 is int) # False
print(type(num2) is int) # True
print(isinstance(num2, int)) # True
파이썬은 첫 줄을 num1 and (num2 is int)로 읽습니다. 2는 값이고 int는 자료형이라 num2 is int는 같을 수가 없으니 항상 False입니다. 자료형 확인은 type(num2) is int나 isinstance(num2, int)로 합니다.
결국 정답은 위의 safe_divide와 같은 모양이었습니다. 문제가 "위 결과에 이어서"라서 숫자 확인에서 끝내지 않고 나누기까지 해야 했습니다. 타입을 미리 검사하는 대신 일단 나눠 보고 ZeroDivisionError와 TypeError를 잡으면 됩니다.
📄 파일 읽고 쓰기: open과 with
open(파일명, 모드)로 열고, 읽거나 쓰고, close()로 닫습니다. 모드 글자 r · w · a는 C의 fopen과 같아서 금방 손에 익었습니다.
| 모드 | 파일이 있으면 | 파일이 없으면 |
|---|---|---|
'r' 읽기 | 읽음 | FileNotFoundError |
'w' 쓰기 | 내용을 싹 지우고 새로 씀 | 새로 만듦 |
'a' 추가 | 끝에 이어서 씀 | 새로 만듦 |
'x' 새로 만들기 | FileExistsError (덮어쓰기 방지) | 새로 만듦 (w와 같음) |
조심할 건 'w'입니다. 이미 있는 파일을 w로 열면 경고 없이 내용이 사라집니다. 덮어쓰면 안 되는 파일이라면 'x'로 열어서 FileExistsError를 받는 쪽이 안전합니다.
f = open('Myfile.txt', 'w')
# 줄바꿈은 \n을 직접 넣어야 함
f.write('안녕하세요?\n')
f.close()
f = open('Myfile.txt', 'a')
# 기존 내용 뒤에 이어 씀
f.write('만나서 반갑습니다.')
f.close()
f = open('Myfile.txt', 'r')
print(f.read())
f.close()
# 안녕하세요?
# 만나서 반갑습니다.
with 문이 기본
with 블록이 끝나면 close()가 자동으로 호출됩니다. close를 깜빡하는 실수가 원천 차단되니 앞으로는 with 문을 기본으로 쓰기로 했습니다. 예외처리에서 finally로 파일을 닫던 일을 with가 대신해 줍니다.
name = ['영철', '옥순', '광수', '영자']
with open('name2.txt', 'w') as f:
for item in name:
f.write(item + '\n') # 한 줄에 한 명씩
# 블록이 끝나면 자동으로 close()
읽기 · 쓰기 메서드
| 메서드 | 하는 일 | 결과 |
|---|---|---|
read() | 전체를 한 번에 | 문자열 하나 |
readline() | 한 줄씩 | 문자열 (다 읽으면 '') |
readlines() | 모든 줄을 한 번에 | 리스트 (줄마다 \n 포함) |
write(문자열) | 문자열 쓰기 | |
writelines(리스트) | 리스트를 이어서 쓰기 |
writelines()는 줄바꿈을 자동으로 넣어 주지 않습니다. 원소마다 \n을 직접 붙여 둬야 줄이 나뉩니다.
hello = ['안녕하세요\n',
'만나서 반가워요\n',
'오늘 날씨가 좋아요.\n']
with open('Myfile5.txt', 'w') as f:
f.writelines(hello)
with open('Myfile5.txt', 'r') as f:
print(f.readlines())
# ['안녕하세요\n', '만나서 반가워요\n',
# '오늘 날씨가 좋아요.\n']
readline()은 다 읽으면 빈 문자열을 돌려줍니다. 그래서 while 조건에 바로 넣을 수 있습니다. C에서 fgets가 NULL을 돌려줄 때까지 도는 루프와 같은 모양입니다.
with open('Myfile5.txt', 'r') as f:
result = f.readline() # 첫 줄
# 빈 문자열('')이 되면 끝
while result:
# 줄 끝에 \n이 이미 있어서 end=''
print(result, end='')
result = f.readline() # 다음 줄
# 안녕하세요
# 만나서 반가워요
# 오늘 날씨가 좋아요.
더 짧은 방법도 있습니다. 파일 객체는 for로 바로 한 줄씩 꺼낼 수 있고, 뒤의 QR 실습도 이 방식을 씁니다.
with open('Myfile5.txt', 'r') as f:
for line in f:
# strip(): 양쪽 공백과 줄바꿈 제거
print(line.strip())
# 안녕하세요
# 만나서 반가워요
# 오늘 날씨가 좋아요.
파일은 어디에 생길까
경로 없이 파일 이름만 쓰면 현재 작업 폴더에 생깁니다. 그 위치는 os로 확인합니다.
import os
print(os.getcwd()) # 현재 작업 폴더 경로
print(os.listdir()) # 현재 폴더에 있는 파일 목록
예제는 Colab 기준입니다. Colab은 일회성 세션이라 다시 접속하면 만든 파일이 날아갑니다. 구글 드라이브를 마운트하면 파일을 저장해 두고 다시 쓸 수 있어서 큰 데이터나 학습 파일을 올려 두기 좋습니다.
# Colab에서만 동작
from google.colab import drive
drive.mount('/content/drive')
저는 맥의 VS Code에서 실습하다가 여기서 한 번 걸렸습니다. 자료에 있던 '/content/Myfile.txt'는 Colab 경로라서 맥에서는 FileNotFoundError가 납니다. 그냥 'Myfile.txt'로 바꾸면 됩니다.
💡 한글이 들어간 파일은 encoding='utf-8'을 붙이는 습관을 들이기로 했습니다. 윈도우는 기본 인코딩이 달라서 한글이 깨질 수 있습니다.
🔳 QR코드: 오늘 배운 걸 한 번에
QR코드는 라이브러리 하나로 끝납니다. qrcode.make(데이터)가 이미지를 만들고 .save()가 저장합니다.
# 터미널 기준. 주피터나 Colab 셀에서는
# 앞에 %를 붙여 %pip install "qrcode[pil]"
# [pil]은 이미지 저장과 색 지정에 쓰는
# Pillow까지 같이 설치
pip install "qrcode[pil]"
import qrcode
img = qrcode.make('안녕하세요 반갑습니다')
img.save('QRcode.png')
txt를 읽어서 QR 여러 장 만들기
함수, 파일 읽기, 예외처리가 한 코드에 다 나옵니다. qrdata.txt에 한 줄에 하나씩 데이터를 적어 두면 줄마다 QR 파일이 하나씩 생깁니다.
import qrcode
def create_qrcode(data):
img = qrcode.make(data)
# 데이터 이름으로 파일 저장
img.save(f'qrcode_{data}.png')
file_path = 'qrdata.txt'
try:
with open(file_path, 'r') as f:
for line in f:
# 줄 끝 \n 제거
data = line.strip()
create_qrcode(data)
except FileNotFoundError:
print(f'Error: {file_path} 파일이 있는지 '
'확인하고 다시 시도하세요')
⚠️ 데이터에 /가 있으면 저장이 실패합니다. https://로 시작하는 주소를 넣으면 파일 이름 속 /를 폴더 구분자로 읽어서 FileNotFoundError가 납니다. www.naver.com처럼 슬래시가 없으면 괜찮습니다.
이 FileNotFoundError는 바깥 except에 그대로 잡힙니다. 그래서 "qrdata.txt 파일이 있는지 확인하라"는 엉뚱한 메시지가 나옵니다. try 범위를 좁게 잡으라는 규칙이 왜 있는지 보여 주는 예입니다.
Wi-Fi 접속 QR
폰 카메라로 찍으면 바로 와이파이에 연결되는 QR도 만들었습니다. 정해진 문자열 형식만 맞추면 됩니다.
WIFI:T:WPA;S:와이파이이름;P:비밀번호;;
| 칸 | 뜻 |
|---|---|
T: | 보안 방식 (WPA) |
S: | 와이파이 이름 (SSID) |
P: | 비밀번호 |
;; | 끝 |
이름이나 비밀번호에 \ ; , : "가 들어 있으면 앞에 \를 붙여야 합니다. 아래의 escape_wifi_value()가 그 일을 합니다. 비밀번호는 getpass()로 받아서 입력하는 글자가 화면에 보이지 않게 했습니다.
import qrcode
# 비밀번호 숨김 입력
from getpass import getpass
# 파일 경로 다루기
from pathlib import Path
def escape_wifi_value(value):
escaped_value = ''
for character in value:
# 특수문자면 앞에 \ 붙이기
if character in '\\;,:"':
escaped_value += '\\'
escaped_value += character
return escaped_value
ssid = input('Wi-Fi 이름(SSID): ')
password = getpass('Wi-Fi 비밀번호: ')
if ssid == '' or password == '':
print('Wi-Fi 이름과 비밀번호를 '
'모두 입력하세요.')
else:
wifi_data = (
'WIFI:T:WPA;'
f'S:{escape_wifi_value(ssid)};'
f'P:{escape_wifi_value(password)};;'
)
img = qrcode.make(wifi_data)
output_path = Path('wifi_qrcode.png')
img.save(output_path)
print('저장 완료:', output_path.resolve())
Path(...).resolve()는 저장된 파일의 전체 경로를 돌려줍니다.
색과 모양 바꾸기: QRCode 클래스
qrcode.make() 대신 QRCode 클래스를 쓰면 칸 크기, 여백, 색을 정할 수 있습니다.
import qrcode
text = 'WIFI:T:WPA;S:MyHome;P:12345678;;'
qr = qrcode.QRCode(
box_size=12, # 작은 네모 한 칸 크기(px)
border=4, # 바깥 여백 (최소 4칸 유지)
)
qr.add_data(text)
qr.make(fit=True) # 데이터 양에 맞게 크기 자동
# 무늬 색, 배경 색
img = qr.make_image(fill_color='navy',
back_color='white')
img.save('wifi_blue.png')
자료 셀에는 오타가 두 군데 있었습니다. fill_color='navy' 뒤에 쉼표가 빠져서 그대로 돌리면 SyntaxError가 나고, 배경 색 인자는 black_color로 적혀 있었습니다. 배경은 back이라 back_color가 맞습니다.
네모 대신 둥근 점을 쓰고 가운데에 로고를 넣을 수도 있습니다.
import qrcode
from qrcode.constants import ERROR_CORRECT_H
from qrcode.image.styledpil \
import StyledPilImage
from qrcode.image.styles.moduledrawers.pil \
import RoundedModuleDrawer
text = 'WIFI:T:WPA;S:MyHome;P:12345678;;'
qr = qrcode.QRCode(
# 로고를 넣으려면 H
error_correction=ERROR_CORRECT_H,
box_size=12,
border=4,
)
qr.add_data(text)
qr.make(fit=True)
img = qr.make_image(
image_factory=StyledPilImage,
# 둥근 모양
module_drawer=RoundedModuleDrawer(),
# (선택) 가운데 로고 파일
embedded_image_path='duck.png',
)
img.save('wifi_logo.png')
로고가 가운데를 덮으면 그만큼 QR 무늬가 가려집니다. 그래서 가려져도 복원되는 비율이 가장 높은 H 레벨을 씁니다.
| 오류 복원 레벨 | 가려져도 복원 | 메모 |
|---|---|---|
ERROR_CORRECT_L | 약 7% | |
ERROR_CORRECT_M | 약 15% | 기본값 |
ERROR_CORRECT_Q | 약 25% | |
ERROR_CORRECT_H | 약 30% | 로고가 가운데를 가려도 읽힘 |
내가 만든 것: 이 블로그 QR
배운 걸로 이 블로그(whiteplace.dev) QR을 만들었습니다. 초록색 버전과 둥근 점 버전 두 개입니다. 아래는 초록색 버전이고, 둥근 버전은 위의 둥근 점 코드에서 로고 줄을 빼고 주소만 바꾼 것입니다.
import qrcode
text = 'https://www.whiteplace.dev'
qr = qrcode.QRCode(box_size=12, border=4)
qr.add_data(text)
qr.make(fit=True)
img = qr.make_image(fill_color='green',
back_color='white')
img.save('my_qr.png')
img.show() # .py에서 미리보기
주소에 https://까지 넣어야 폰 카메라가 링크로 인식해서 바로 이동합니다. 주피터에서 쓰던 display()는 주피터 전용이라, .py 파일에서는 img.show()로 미리보기를 띄웁니다.
오늘의 한 줄
try · except는 오류가 나도 프로그램이 꺼지지 않게 하는 장치이고, except 뒤에는 예외 이름을 적습니다. finally는 무조건 실행되고, raise는 직접 오류를 냅니다. 파일은
with open(...) as f로 열면 닫기가 자동이고, w는 덮어쓰기 · a는 이어쓰기입니다. QR은qrcode.make(data).save('파일.png')한 줄이고, 로고를 넣으려면ERROR_CORRECT_H입니다.
QR 실습은 오늘 배운 게 총출동하는 코드였습니다. 함수로 QR을 만들고, with로 txt를 읽고, 파일이 없으면 except로 받습니다. 직접 해 보려면 txt에 주소 몇 줄을 적고 create_qrcode 코드를 돌려 보면 됩니다. https:// 주소를 넣을 거라면 파일 이름을 만들 때 /를 _ 같은 글자로 바꿔 두면 됩니다.