순서 있는 독서 목록
80분 안팎
학습 목표
리스트의 추가·순회와 중복을 설명합니다.
개념
왜 목록인가
독서 기록 한 건은 제목·쪽수·금액·완료 표시를 가진 딕셔너리입니다. 여러 건을 입력 순서대로 보려면 이를 리스트에 담습니다. 신입 코드 리뷰에서 자주 보는 실수는 제목을 변수 여러 개로 나누거나 한 변수에 계속 덮어쓰는 것입니다. 리스트는 길이가 바뀌어도 같은 반복 코드로 모든 기록을 다룰 수 있게 합니다. 이번 목표는 입력 순서를 보존하면서 완료한 기록만 새 목록에 담는 것입니다. 같은 제목을 다시 읽은 기록도 별도 원소로 남겨야 하므로 임의로 중복을 없애지 않습니다.
위치와 의미를 구분합니다
records[0]은 첫 기록이며 첫 제목은 records[0]["title"]입니다. 첫 대괄호의 0은 위치이고 두 번째 대괄호의 title은 필드 이름입니다. 마지막 위치는 len(records)-1이므로 길이를 그대로 인덱스로 사용하면 IndexError가 납니다. 빈 목록에는 첫 기록이 없습니다. 첫 원소를 읽기 전에 if records로 비어 있지 않은지 검사하거나 목록 전체를 for로 순회합니다. 순회는 빈 목록에서 본문을 한 번도 실행하지 않으므로 자연스럽게 빈 결과를 만듭니다.
추가와 반환을 구분합니다
append는 전달한 한 객체를 목록 끝에 추가합니다. records = records.append(record)라고 쓰면 records가 None을 가리킵니다. append는 원본을 변경하고 None을 반환하므로 records.append(record)만 호출합니다. 다음 줄에서 None을 순회하려 하면 TypeError가 보일 수 있습니다. traceback의 마지막 줄만 보지 말고 records에 마지막으로 대입한 줄을 확인합니다. 문자열 하나를 extend하면 문자들이 각각 들어가므로 제목 하나를 넣는 목적에는 append를 사용합니다.
완료 필터를 단계로 만듭니다
result = []로 결과를 준비한 뒤 for record in records로 순회합니다. record["completed"] == 1일 때만 result.append(record["title"])를 수행합니다. 이렇게 만든 결과에는 통과한 제목이 입력 순서대로 들어갑니다. 완료 0은 건너뛰지만 쪽수 0은 완료 여부의 판단 기준이 아닙니다. 무료 책인지 읽은 쪽수가 많은지도 이 필터의 조건이 아닙니다. 한 조건을 구현할 때 다른 필드를 추측해 대신 사용하지 않는 것이 업무 규칙을 지키는 첫 단계입니다.
순회 중 원본을 삭제하지 않습니다
완료하지 않은 기록을 원본에서 remove하는 방식은 뒤 원소가 앞으로 이동해 일부 원소를 건너뛸 수 있습니다. 조회 목적이라면 새 결과 목록을 만들고 원본을 보존합니다. 이번 브라우저 과제는 제목 문자열만 결과에 담으므로 기록 자체를 수정하지 않습니다. 결과가 없으면 EMPTY 한 줄을 출력합니다. 빈 원본과 원본은 있지만 완료 기록이 없는 경우는 같은 출력이지만 테스트에서 둘을 따로 확인해야 서로 다른 실행 경로를 검증할 수 있습니다.
중복 제목은 오류가 아닙니다
독서 기록은 책 목록과 다릅니다. 같은 책을 두 번 읽고 기록했다면 두 건이라는 요구가 자연스럽습니다. 리스트는 같은 값이 여러 번 나와도 허용합니다. set으로 바꾸면 중복과 기록 순서에 관한 약속이 바뀝니다. 중복 거부는 다음 레슨의 ID에 적용하며 제목에는 적용하지 않습니다. 빈 제목과 같은 제목은 다른 문제입니다. 빈 제목은 앞 모듈의 입력 검증으로 거부하고 같은 제목은 별도 기록으로 유지하는 책임을 구분합니다.
출력 형식을 좁힙니다
입력은 첫 줄 정수 n 뒤 제목 한 줄과 완료 표시 한 줄을 n번 받습니다. n은 0 이상이고 완료 표시는 0 또는 1입니다. 완료 제목을 각각 한 줄로 출력하며 없을 때만 EMPTY를 씁니다. for _ in range(n)의 밑줄은 순서 번호를 사용하지 않는다는 관례입니다. 결과 리스트를 print(result)로 출력하면 대괄호와 따옴표가 섞여 요구와 달라집니다. for title in result: print(title)로 계약을 맞추고 한글 제목과 내부 공백이 유지되는지 확인합니다.
다음 조회 기능과 연결합니다
리스트 위치를 기록 ID로 사용하면 중간 삭제나 정렬 후 위치가 달라집니다. 지금의 목록 순서는 화면에 보여 주는 순서이며 영구적인 식별자가 아닙니다. 다음 레슨에서는 book-001 같은 문자열 ID를 별도로 부여합니다. 미션의 list_records는 ID 저장소에서 순서 있는 리스트를 만들어 기존 요약 함수에 전달합니다. 기존 total_pages 함수는 각 원소에 pages 필드가 있으면 ID 필드가 추가되어도 그대로 계산합니다. 구조를 확장하되 소비자의 필요한 필드를 보존합니다.
완료 조건을 표로 확인합니다
완료 0인 달빛, 완료 1인 별빛, 완료 1인 달빛을 입력하면 별빛과 달빛만 그 순서로 나옵니다. 첫 기록을 건너뛰었다고 결과 위치가 원본 위치와 같을 필요는 없습니다. 결과는 조건을 통과한 값의 새 목록입니다. 완료 표시를 문자열로 읽었다면 정수 1과 같지 않을 수 있으므로 제공된 int 변환을 유지합니다. 문자열 0은 빈 문자열이 아니어서 참으로 평가되므로 if completed만 쓰는 방식도 계약에 맞는지 주의해야 합니다. 여기서는 정수 변환 뒤 == 1을 명시합니다.
원본 보존을 눈으로 확인합니다
필터 호출 전 제목 세 개를 별도 목록에 적고 호출 후 원본을 다시 출력합니다. 결과에 두 개가 나왔다고 원본도 두 개가 되면 조회가 삭제 기능으로 바뀐 것입니다. 첫 단계의 append 실험과 달리 필터는 입력 변경을 허용하지 않는 함수로 만듭니다. 빈 목록과 모두 미완료 목록도 추가해 EMPTY가 나오는지 비교합니다. 중복 제목의 기대값을 직접 적으면 set을 잘못 선택했을 때 한 제목이 사라지는 현상을 확인할 수 있습니다. 건수와 제목 나열을 함께 봅니다.
목록 반환을 요약에 전달합니다
미션에서는 list_records 결과를 total_pages나 completed_count에 전달할 수 있습니다. 반환 목록의 각 기록에는 기존 pages와 completed 필드가 유지됩니다. 딕셔너리 원본을 바로 total_pages에 넘기면 반복 원소는 기록이 아니라 ID 키가 되어 오류가 날 수 있습니다. 함수가 받는 자료형을 맞춰 주는 변환을 호출부에서 분명히 합니다. 테스트 이름이 summary인 기존 검사가 실패하면 새 ID 구현만 보지 말고 목록에 어떤 원소가 들어 있는지 출력으로 좁혀 봅니다. 최종 코드에서는 관찰용 출력을 제거합니다.
따라하기
끝에 추가
다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.
titles = ["달빛"]
titles.append("별빛")
titles.append("달빛")
print(titles)
print(len(titles))
실행 결과
['달빛', '별빛', '달빛'] 3
완료만 남기기
다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.
records = [{"title": "달빛", "completed": 0}, {"title": "별빛", "completed": 1}]
result = []
for record in records:
if record["completed"] == 1:
result.append(record["title"])
print(result)
print(len(records))
실행 결과
['별빛'] 2
빈 목록 경계
다음 코드를 demo.py에 저장하고 python3 demo.py로 실행합니다. 단계마다 파일 전체를 교체하므로 이전 단계의 변수에 의존하지 않습니다.
records = []
for record in records:
print(record)
print("EMPTY" if not records else "FOUND")
try:
records[0]
except IndexError as error:
print(type(error).__name__, str(error))
실행 결과
EMPTY IndexError list index out of range
확인 문제
실습
첫 줄 n 뒤 제목 한 줄·완료 0 또는 1 한 줄을 n번 읽습니다. 입력은 유효합니다. 완료 1인 제목을 입력 순서대로 한 줄씩 출력하며 중복을 유지합니다. 없으면 EMPTY 한 줄입니다. result 필터 코드를 완성하고 records는 수정하지 않습니다.
모범 답안
n = int(input())
records = []
for _ in range(n):
title = input()
completed = int(input())
records.append({"title": title, "completed": completed})
result = []
for record in records:
if record["completed"] == 1:
result.append(record["title"])
if result:
for title in result:
print(title)
else:
print("EMPTY")
더 읽기
면접 질문
- 리스트 대신 딕셔너리를 선택하는 사례를 설명합니다.