변수와 반복으로 샘플 읽기
85분 안팎
학습 목표
입력한 샘플의 개수·최솟값·최댓값을 계산합니다.
개념
여러 샘플을 요약하는 작은 상태
기록기는 한 번 읽은 숫자보다 시간에 따라 쌓이는 샘플을 다룹니다. 이번에는 입력 토큰을 차례대로 검사하면서 유효 샘플의 개수, 최솟값, 최댓값을 계산합니다. 전체 기록을 정렬하지 않고도 양 끝을 구할 수 있습니다. 목표는 요약 숫자를 얻는 데 그치지 않고 다음 샘플이 들어올 때 어떤 변수를 바꿔야 하는지 설명하는 것입니다. 이후 C CLI에서도 같은 집계 규칙을 유지합니다.
입력은 공백으로 구분한 십진 정수들이며 빈 입력도 허용합니다. 예를 들어 12 -1 8 12에서 -1은 센서 범위 밖이라 집계하지 않고 12 두 개는 각각 한 건입니다. 출력은 SUMMARY 3 8 12 한 줄입니다. 유효한 값이 없다면 SUMMARY 0 NONE NONE입니다. 여기서 NONE은 실제 센서 값이 아니라 요약값이 존재하지 않는다는 표시입니다. 정상 0과 미존재를 다른 표현으로 구분합니다.
한 줄을 읽는 input() 대신 sys.stdin.read()로 입력이 끝날 때까지 받은 문자열을 읽습니다. split()에 인자를 주지 않으면 연속 공백, 탭, 줄바꿈을 구분자로 보고 토큰을 얻습니다. 빈 문자열의 split() 결과는 빈 리스트입니다. sys는 Python 표준 라이브러리이므로 별도 설치는 필요하지 않습니다. read()를 사용하는 PC 모형은 입력 종료 뒤 계산하는 배치 실행이며 실제 무한 센서 스트림을 읽는 구조는 아닙니다.
변수마다 지킬 약속을 정합니다
count는 지금까지 수락한 샘플 수입니다. minimum과 maximum은 수락한 값들의 양 끝입니다. 첫 샘플을 아직 만나지 못했을 때 양 끝은 None으로 둡니다. None을 숫자 0과 혼동하지 않습니다. Python에서는 minimum is None으로 초기 상태를 확인할 수 있습니다. count가 0일 때만 미정 상태여야 하고, 정상값을 하나 수락하면 두 경계가 모두 정수가 된다는 약속을 유지합니다.
반복문 for token in tokens는 문자열 토큰을 하나씩 가져옵니다. int(token)으로 바꾼 raw가 정상 범위를 벗어나면 continue로 이번 반복의 나머지를 건너뜁니다. count를 늘리는 줄은 범위 검사 뒤에 놓습니다. 앞에서 늘리면 거부한 샘플도 건수에 섞입니다. continue는 프로그램 종료가 아니라 다음 토큰으로 이동하는 동작입니다. 잘못된 샘플 하나 때문에 뒤의 정상 샘플을 놓치지 않도록 break와 구분합니다.
첫 정상값은 minimum과 maximum에 같은 값을 넣습니다. 다음 정상값이 minimum보다 작으면 minimum만 바꾸고, maximum보다 크면 maximum만 바꿉니다. 동일한 값은 경계를 바꾸지 않지만 count는 증가합니다. 집계할 때 필요한 것은 이전 요약과 새 값이므로 별도의 정렬 목록을 만들 필요가 없습니다. 이번 Python 입력 읽기는 전체 문자열을 담지만 집계 변수의 개수 자체는 샘플 건수에 따라 늘지 않습니다.
최솟값을 0으로 초기화하면 모든 입력이 양수인 경우 존재하지 않는 0을 결과로 내게 됩니다. 최대값을 4095로 초기화해도 모든 입력이 그보다 작을 때 실제로 없던 4095가 남습니다. 초기값을 예상 범위의 끝으로 두고 잊는 방식보다 첫 유효값으로 양쪽을 시작하면 의미를 설명하기 쉽습니다. 유효값이 없을 때 별도 출력을 하는 것도 첫 값 초기화와 함께 설계합니다.
손으로 상태를 따라가면 누락이 보입니다
입력 12 -1 8 12를 처리하는 순서를 종이에 적습니다. 12 뒤에는 count=1, minimum=12, maximum=12입니다. -1 뒤에는 세 값이 그대로입니다. 8 뒤에는 count=2, minimum=8, maximum=12이고 마지막 12 뒤에는 count만 3으로 바뀝니다. 프로그램의 각 반복 뒤 변수 상태와 이 표를 비교하면 잘못된 건수 증가나 최대값 갱신 위치를 찾을 수 있습니다.
처음부터 복잡한 입력을 넣으면 실패한 위치를 찾기 어렵습니다. 먼저 한 값 7로 초기화를 확인하고, 7 3으로 최솟값 갱신, 7 9로 최댓값 갱신을 확인합니다. 그 다음 중복값과 범위 밖 값, 빈 입력을 조합합니다. 정렬된 입력만 사용하면 마지막 값이 항상 최대라는 잘못된 코드도 통과하므로 순서를 바꾸어 실행합니다. 요약은 같은 값들이면 입력 순서에 관계없이 같아야 합니다.
경계 테스트는 0 4095에서 SUMMARY 2 0 4095가 되어야 합니다. -1 4096에서는 count가 0이므로 NONE이 양쪽에 나옵니다. 빈 입력과 공백만 있는 입력도 같은 요약이지만 이유가 다릅니다. 전자는 토큰이 없고 후자는 split이 공백을 제거해서 토큰이 없습니다. 숫자 0 한 개의 요약은 SUMMARY 1 0 0이므로 빈 입력 결과와 다릅니다. 이 세 결과를 나란히 확인합니다.
출력에는 int로 유지한 count와 경계를 사용하므로 0.0 같은 소수 표시는 나오지 않습니다. SUMMARY 다음에는 개수, 최소, 최대 순서를 지킵니다. 디버깅할 때 중간 상태를 표준 출력에 추가하면 정답 결과와 섞이므로 실행을 마치기 전에 제거합니다. 최종 결과는 한 줄만 내야 합니다. 문자 NONE을 Python 객체 None 그대로 출력해서 None으로 바꾸는 실수도 계약 불일치입니다.
반복이 멈추거나 예외가 날 때 읽는 곳
빈 입력에서 IndexError가 난다면 첫 토큰을 검사 없이 tokens[0]으로 읽었는지 확인합니다. 처음 값이 없을 때도 반복문은 자연스럽게 아무 일도 하지 않아야 합니다. minimum과 raw를 바로 비교하다 TypeError가 나오면 minimum이 None인 초기 상태를 분리했는지 봅니다. 첫 값 여부를 먼저 확인하고 그 다음 숫자를 비교하는 순서가 중요합니다. 예외를 숨기는 대신 아직 정수가 아닌 변수를 찾습니다.
for 다음 갱신 문장을 들여쓰지 않으면 마지막 토큰 하나만 처리하거나 반복 바깥에서 이름을 읽는 오류가 생깁니다. 반대로 SUMMARY 출력을 반복 안에 넣으면 샘플마다 요약이 여러 줄 나옵니다. 반복은 값 검사와 상태 갱신, 반복 뒤는 최종 요약이라는 경계를 코드에 표시합니다. count = 1을 반복마다 대입하면 누적되지 않으므로 count += 1로 이전 값에 하나를 더합니다.
범위 조건을 raw < 0 and raw > 4095로 쓰면 어느 정수도 동시에 두 조건을 만족하지 않습니다. 거부 조건은 하한 미만 또는 상한 초과이므로 or를 사용합니다. 수락 조건에서는 양쪽을 동시에 만족하는 and 또는 연쇄 비교를 씁니다. 말로 외우기보다 -1과 4096을 조건식에 각각 대입해 참과 거짓을 계산합니다. 정상 내부값 12도 대입하면 반대로 쓴 조건을 확인할 수 있습니다.
실습 starter에는 건수를 모든 토큰 수로 정하고 경계를 0으로 고정한 코드가 있습니다. 입력 형식은 제공되어 있으니 정상값만 집계하는 반복으로 바꿉니다. 테스트에는 순서 변화와 중복, 두 끝, 빈 입력이 들어 있습니다. 통과 뒤에는 집계에 필요한 상태가 왜 세 개인지 말로 설명합니다. 입력 전체 보관이 필요한 경우와 누적 요약만 필요한 경우의 차이는 다음 모듈의 고정 버퍼를 설계할 때 다시 사용합니다.
따라하기
첫 유효값으로 시작
입력 칸에 7을 넣고 한 표본이 양쪽 경계를 정하는지 봅니다.
import sys
count = 0
minimum = None
maximum = None
for token in sys.stdin.read().split():
raw = int(token)
if raw < 0 or raw > 4095:
continue
if minimum is None or raw < minimum:
minimum = raw
if maximum is None or raw > maximum:
maximum = raw
count += 1
if count == 0:
print("SUMMARY 0 NONE NONE")
else:
print("SUMMARY", count, minimum, maximum)
실행 결과
SUMMARY 1 7 7
거부값과 중복을 섞기
12 -1 8 12를 넣어 반복 상태를 본문 표와 대조합니다.
import sys
count = 0
minimum = None
maximum = None
for token in sys.stdin.read().split():
raw = int(token)
if raw < 0 or raw > 4095:
continue
if minimum is None or raw < minimum:
minimum = raw
if maximum is None or raw > maximum:
maximum = raw
count += 1
if count == 0:
print("SUMMARY 0 NONE NONE")
else:
print("SUMMARY", count, minimum, maximum)
실행 결과
SUMMARY 3 8 12
아무 값도 없을 때
입력 칸을 비운 채 실행하여 미존재 표시를 확인합니다.
import sys
count = 0
minimum = None
maximum = None
for token in sys.stdin.read().split():
raw = int(token)
if raw < 0 or raw > 4095:
continue
if minimum is None or raw < minimum:
minimum = raw
if maximum is None or raw > maximum:
maximum = raw
count += 1
if count == 0:
print("SUMMARY 0 NONE NONE")
else:
print("SUMMARY", count, minimum, maximum)
실행 결과
SUMMARY 0 NONE NONE
확인 문제
실습
공백으로 나눈 정수 토큰들을 읽고 0~4095만 집계합니다. SUMMARY 개수 최소 최대 한 줄을 출력합니다. 중복도 각각 세며 유효값이 없으면 SUMMARY 0 NONE NONE입니다. 입력 토큰은 십진 정수 형식입니다.
모범 답안
import sys
count = 0
minimum = None
maximum = None
for token in sys.stdin.read().split():
raw = int(token)
if raw < 0 or raw > 4095:
continue
if minimum is None or raw < minimum:
minimum = raw
if maximum is None or raw > maximum:
maximum = raw
count += 1
if count == 0:
print("SUMMARY 0 NONE NONE")
else:
print("SUMMARY", count, minimum, maximum)
더 읽기
면접 질문
- 첫 샘플 전의 상태와 정상 원시값 0을 집계에서 어떻게 구별하나요?