숫자가 아닌 입력 처리
75분 안팎
학습 목표
변환 오류와 업무 규칙 위반을 구분합니다.
개념
왜 실패를 값으로 설명하는가
독서 기록에는 0쪽과 0원도 들어갑니다. 아직 읽지 않은 책이나 증정 도서를 기록할 수 있기 때문입니다. abc를 입력했는데 0원으로 저장하면 정상 기록과 실패가 섞입니다. 프로그램이 종료되지 않았다는 사실보다 데이터 의미를 지켰는지가 중요합니다. 이번 목표는 정상 정수와 빈 입력·숫자가 아닌 입력·음수를 구분하는 파서를 만들고 호출부에서 실패를 확인하는 것입니다. 성공은 정수와 None, 실패는 None과 오류 코드의 쌍으로 반환합니다.
왜 int만으로 계약을 정하지 않는가
int는 문자열을 정수로 바꾸지만 우리 프로그램이 허용할 표기는 따로 정합니다. 앞 모듈은 앞뒤 공백과 선택적인 + 또는 - 뒤 ASCII 숫자를 허용했습니다. 이번에도 그 규칙을 유지합니다. 1.5, 1_000, 전각 숫자는 NOT_INTEGER로 거절합니다. strip 후 빈 문자열인지 먼저 보고 re.fullmatch로 문자열 전체의 표기를 확인합니다. 패턴 [+-]?[0-9]+는 부호 0개 또는 1개 뒤 숫자 1개 이상이라는 뜻입니다. search는 일부만 맞아도 성공하므로 이 검사에 맞지 않습니다.
변환 실패와 규칙 위반
try에는 int(text)만 놓고 except ValueError에서 변환 실패를 결과로 바꿉니다. 숫자 표기를 먼저 확인해도 환경의 긴 정수 문자열 제한 등으로 ValueError가 발생할 수 있으므로 변환 자체의 실패도 처리합니다. 음수는 변환에 성공한 뒤 value < 0으로 거절합니다. -0은 정수 0이므로 성공합니다. 금액은 정수 원 단위이고 소수 금액을 반올림하거나 음수를 절댓값으로 고치는 정책은 없습니다. 잘못된 값을 추측해서 고치기보다 수정할 이유를 알려 줍니다.
잡는 범위를 좁히는 이유
except Exception이나 except:로 전체 함수를 감싸면 필드 이름 오타나 예상하지 않은 자료형도 사용자 입력 오류로 보일 수 있습니다. 이 파서는 문자열을 받는다는 계약입니다. None을 전달한 호출자의 실수는 AttributeError로 드러날 수 있고 이번 파서는 그것을 EMPTY로 바꾸지 않습니다. try 안에 저장이나 합계 계산을 넣지 않습니다. 이 함수는 출력과 목록 변경 없이 입력을 해석하므로 다른 곳에서 같은 결과를 재사용할 수 있습니다.
흔한 실수: 참 거짓으로 성공 판단
if not value:는 성공 값 0도 거짓으로 봅니다. if error is not None:으로 실패를 확인한 뒤 성공 값을 씁니다. return을 빠뜨리면 함수 전체가 None을 반환해 두 변수로 풀어 받을 때 TypeError가 날 수 있습니다. 반환 쌍의 순서가 바뀌어도 호출부의 판단이 틀립니다. 정상·실패 양쪽 모두 같은 모양을 돌려주고 함수 안에서 print하지 않습니다. 실행 중 ValueError 메시지를 보았다면 실패를 처리할 int 줄과 입력 원문을 먼저 찾습니다.
미션에 옮기는 순서
앞 모듈의 app.py, batch.py, summary.py와 tests를 보존합니다. 새 parsing.py는 문자열을 해석하고 records.py는 검증된 한 건을 추가합니다. records는 제목·pages·cost·completed를 가진 딕셔너리들의 리스트입니다. 대괄호로 필드를 읽고 append로 끝에 한 건을 넣습니다. 구조 선택의 상세한 이유는 다음 모듈에서 다룹니다. 이번에는 공유 목록을 바꾸는 시점에 집중합니다. 쪽수만 맞다고 추가하지 않고 제목·쪽수·금액·완료 표시가 모두 통과한 뒤 한 번 추가합니다.
오류 코드를 읽고 다음 행동을 정합니다
EMPTY는 공백 제거 후 입력이 비었다는 뜻이며 수량 0과 다릅니다. NOT_INTEGER는 정수 표기 계약을 만족하지 않는 경우입니다. NEGATIVE는 정수로 해석했지만 음수인 경우입니다. UI는 코드를 사용자 안내로 바꿀 수 있고 테스트는 코드 자체를 비교합니다. 실패를 0으로 바꾸면 무료 책과 실패한 금액을 구별할 수 없습니다. 반환값을 두 변수로 받는 value, error = parse_number(raw)는 한 쌍의 값을 풀어 받는 문법입니다. error가 None일 때만 value를 계산에 씁니다.
작은 검사와 전체 검사를 연결합니다
하나의 예제가 맞아도 다른 입력까지 보장하지 않습니다. 실패했던 입력을 먼저 고정해 기대값을 적고 함수 검사로 원인을 좁힙니다. 수정 후 같은 검사를 다시 실행하고 기존 한 건 입력·빈 요약·파일 기준 경로 검사까지 실행합니다. unittest discover의 기본 파일 패턴은 test*.py이며 TestCase를 상속한 클래스의 test로 시작하는 메서드를 실행합니다. 0개 실행으로 끝난 성공은 이 미션의 완료 근거가 아닙니다. FAIL은 비교 불일치, ERROR는 검사 중 예상하지 못한 예외가 발생한 신호입니다. 마지막 OK뿐 아니라 실행 개수도 확인합니다.
따라하기와 제출의 차이
단계의 코드는 서로 독립된 작은 실험입니다. 표시된 코드를 파일에 저장하고 실행하면 출력과 비교할 수 있습니다. 브라우저 실습에서는 안내 문구 없이 표준 입력을 읽으며 계약의 결과만 출력합니다. input()은 줄 끝 개행을 제거하지만 문자열 내부 공백까지 지우지는 않습니다. 빈 입력을 시험할 때는 입력 줄 하나를 비워 둡니다. 입력 자체가 없는 EOFError와 빈 줄을 혼동하지 않습니다. 실험용 print를 제출에 남기면 채점 출력이 달라지므로 최종 결과 출력만 남깁니다.
검증 순서의 이유
빈 값 검사를 정규식보다 먼저 하면 사용자가 무엇을 채워야 하는지 구체적으로 알려 줄 수 있습니다. 음수 표기를 숫자 아닌 입력으로 분류하지 않으려면 부호를 표기에서 허용한 다음 범위를 검사합니다. 정상 값은 계산에 쓰고 오류 코드는 안내에 쓰므로 두 역할을 섞지 않습니다. 문자열 계약을 함수 설명에 적어 호출자의 책임도 분명히 합니다.
검토할 때 묻는 질문
결과를 정수로 받은 뒤 어디서 기존 목록에 반영되는지 찾아봅니다. 함수 이름만 보고 안전하다고 판단하지 않고 return 전의 모든 변경 줄을 읽습니다. 호출이 실패했을 때 실행이 멈춘 위치와 이미 실행된 변경을 나누어 적습니다. 성공·실패 출력뿐 아니라 이후 상태까지 비교하면 겉으로 정상처럼 보이는 손상을 발견할 수 있습니다.
따라하기
변환 오류 관찰
다음 코드를 별도 파일 demo.py에 저장하고 python3 demo.py로 실행합니다.
try:
int("abc")
except ValueError as error:
print(type(error).__name__)
print(str(error))
실행 결과
ValueError invalid literal for int() with base 10: 'abc'
세 실패 구분
다음 코드를 별도 파일 demo.py에 저장하고 python3 demo.py로 실행합니다.
import re
def parse_number(raw):
text = raw.strip()
if not text:
return None, "EMPTY"
if not re.fullmatch(r"[+-]?[0-9]+", text):
return None, "NOT_INTEGER"
try:
value = int(text)
except ValueError:
return None, "NOT_INTEGER"
if value < 0:
return None, "NEGATIVE"
return value, None
for raw in ["", "abc", "-1", "0"]:
print(repr(raw), parse_number(raw))
실행 결과
'' (None, 'EMPTY') 'abc' (None, 'NOT_INTEGER') '-1' (None, 'NEGATIVE') '0' (0, None)
0원을 성공으로 처리
다음 코드를 별도 파일 demo.py에 저장하고 python3 demo.py로 실행합니다.
import re
def parse_number(raw):
text = raw.strip()
if not text:
return None, "EMPTY"
if not re.fullmatch(r"[+-]?[0-9]+", text):
return None, "NOT_INTEGER"
try:
value = int(text)
except ValueError:
return None, "NOT_INTEGER"
if value < 0:
return None, "NEGATIVE"
return value, None
value, error = parse_number("0")
if error is None:
print("OK|" + str(value))
else:
print(error)
실행 결과
OK|0
확인 문제
실습
금액 문자열 한 줄을 읽습니다. 앞뒤 공백 제거 후 빈 값은 EMPTY, 부호가 선택적으로 붙은 ASCII 정수 표기가 아니면 NOT_INTEGER, 정수 음수는 NEGATIVE를 출력합니다. 성공은 OK|값입니다. 0과 -0은 성공합니다. 파서는 (값, 오류) 쌍을 반환하고 출력은 호출부에서 합니다.
모범 답안
import re
def parse_number(raw):
text = raw.strip()
if not text:
return None, "EMPTY"
if not re.fullmatch(r"[+-]?[0-9]+", text):
return None, "NOT_INTEGER"
try:
value = int(text)
except ValueError:
return None, "NOT_INTEGER"
if value < 0:
return None, "NEGATIVE"
return value, None
value, error = parse_number(input())
print(error if error is not None else f"OK|{value}")
더 읽기
면접 질문
- 지출 합계 프로그램의 잘못된 입력 처리 방식을 설명합니다.