교재 홈: 바이브 코더를 위한 코딩 기초 학습
이 장에서는 여러 값을 함께 담는 방법인 **자료구조(data structure)**를 배웁니다. 장보기 목록처럼 순서대로 적을 수도 있고, ‘제목: 질문의 기술’처럼 이름과 값을 짝지을 수도 있습니다. 순서 보존은 들어 있던 차례를 유지하는 것이고 중복 보존은 같은 값이 두 번 있었다는 사실을 남기는 것입니다. 작업에 필요한 정보가 무엇인지에 따라 묶는 방식을 고릅니다.
**원소(element)**는 묶음 안에 들어 있는 값 하나입니다. **리스트(list)**는 순서대로 놓은 값의 목록, **튜플(tuple)**은 원소 자리를 바꾸지 않는 순서 묶음, **딕셔너리(dictionary)**는 이름과 값을 짝지은 묶음, **집합(set)**은 같은 값을 한 번씩만 담는 묶음입니다. 딕셔너리의 이름 쪽을 **키(key)**라고 하며, 실제 열쇠가 아니라 원하는 값을 찾는 표지라는 뜻입니다. 다른 언어에서 비슷한 구조를 **맵(map)**이라고 부르기도 합니다. 용어보다 아래 예에서 순서·중복·수정 가능성이 어떻게 다른지 봅니다.
| 구조 | 예시 | 선택 기준 |
|---|---|---|
리스트 list |
[20, 0, 35] |
순서와 중복을 보존하며 값을 추가·수정합니다. |
튜플 tuple |
("R001", 30) |
순서가 있는 고정 묶음입니다. 원소 자리를 재대입할 수 없습니다. |
딕셔너리 dict |
{"title": "가상 도서", "minutes": 30} |
이름인 키로 값을 찾습니다. 같은 키는 하나만 존재합니다. |
집합 set |
{"철학", "소설"} |
중복 없는 값의 소속 여부를 다룹니다. 출력 순서를 전제하지 않습니다. |
원소가 한 개인 튜플을 만들 때에는 ("R001",)처럼 값 뒤에 쉼표를 붙입니다. ("R001")은 문자열 "R001"을 괄호로 감싼 표현이므로 튜플이 아닙니다. 원소가 없는 튜플은 ()로 적습니다. 괄호 안의 공백 유무는 이 구별에 영향을 주지 않습니다.
다른 언어의 배열(array)을 읽을 때 Python 리스트와 비슷한 역할을 떠올릴 수 있지만, 크기와 원소 자료형에 관한 규칙은 언어마다 다릅니다. Python 리스트는 서로 다른 자료형도 담을 수 있습니다. 허용된다는 것과 한 목록에 섞는 것이 읽기 쉽다는 것은 다릅니다.
다음 예에서 **기록(record)**은 한 번의 독서에 관한 값 묶음입니다. id는 기록을 구별하려고 붙인 식별자, title은 제목, minutes는 시간입니다. **식별자(identifier)**는 다른 것과 구별하기 위해 붙인 이름이나 번호입니다. **필드(field)**는 한 기록 안에서 제목이나 시간처럼 정해진 역할을 가진 항목입니다. 목록 안에 기록 묶음을 넣듯 묶음 안에 다른 묶음을 넣는 구성을 **중첩(nesting)**이라고 합니다.
코드에서 대괄호 [...]는 리스트를 만들거나 위치·키로 값을 찾을 때 쓰고, 중괄호 {...} 안에 키: 값을 적으면 딕셔너리가 됩니다. records는 여러 기록을 담은 목록입니다.
structures.py에 저장합니다.
records = [
{"id": "R001", "title": "논증의 구조", "minutes": 30},
{"id": "R002", "title": "지식과 교육", "minutes": 45},
{"id": "R003", "title": "질문의 기술", "minutes": 0},
]
print(records[0]["title"])
print(records[-1]["minutes"])
print(records[0].get("memo"))
print([record["minutes"] for record in records])
출력은 논증의 구조, 0, None, [30, 45, 0]입니다. 바깥 리스트에는 여러 기록이, 안쪽 딕셔너리에는 한 기록의 항목이 있습니다. 리스트에서 위치를 가리키는 번호가 **인덱스(index)**입니다. Python 리스트는 첫 위치를 0으로 세므로 records[0]이 첫 기록입니다. 거기에 ["title"]을 붙이면 첫 기록의 제목을 찾습니다.
음수인 인덱스는 끝에서부터 셉니다. -1은 마지막, -2는 뒤에서 두 번째입니다. 빈 목록에는 첫 값도 마지막 값도 없으므로 [0]이나 [-1]을 읽으면 오류가 납니다. None은 찾을 값이 없음을 나타내며 숫자 0과 다릅니다.
마지막 줄의 **리스트 컴프리헨션(list comprehension)**은 ‘각 기록에서 시간을 꺼내 새 목록으로 모으기’를 한 줄에 적는 문법입니다. for record in records는 기록을 하나씩 읽는 부분이고, 앞의 record["minutes"]는 새 목록에 넣을 값입니다. 처음에는 일반 반복문을 머릿속으로 떠올려 읽으면 됩니다. 익숙하지 않으면 이 표현을 스스로 만들 필요 없이 일반 반복문으로 풀어 설명해도 됩니다.
**기본값(default value)**은 값이 지정되지 않았을 때 대신 사용하도록 정한 값입니다. **결측값(missing value)**은 필요한 자료가 비어 있는 경우입니다. ‘독서를 하지 않아 0분이라고 적음’과 ‘시간을 적지 않음’은 의미가 다릅니다. 관측값은 실제로 측정하거나 기록한 값이라는 뜻이며 과학 실험에서만 쓰는 말은 아닙니다. 입력과 출력의 전체 흐름은 05. 실행 구조 — 어디서 시작해 무엇이 바뀌는가의 「입력·상태·출력」절에서 이어집니다.
record["memo"]는 memo라는 키가 없으면 KeyError를 발생시킵니다. 이 이름은 ‘요청한 키를 찾지 못했다’는 오류 종류입니다. record.get("memo")는 키가 없으면 기본적으로 None을 돌려줍니다. .get()은 딕셔너리에 준비된 동작인 **메서드(method)**의 하나이며, 함수처럼 괄호를 붙여 사용합니다. 메서드와 객체는 바로 다음 절에서 설명합니다. 오류 종류와 처리 방법은 07. 오류 처리 — 메시지에서 원인 후보로 가기의 「오류의 세 종류」절에서 배웁니다. 필수 항목의 누락까지 기본값으로 덮으면 입력 문제를 놓칠 수 있습니다.
record.get("minutes", 0)은 키가 없을 때 0을 돌려줍니다. 이때 ‘기록하지 않음’과 ‘실제 0분’이 같아집니다. 교재의 독서 기록에서는 시간 누락을 오류로 분류하고 0은 유효한 관측값으로 보존합니다. 결측 처리 규칙은 자료구조의 기능이 대신 결정해 주지 않습니다.