인덱스로 찾는 것의 한계
리스트로도 되기는 합니다. 되기만 합니다.
딕셔너리 없이 학생마다 점수를 저장할 때 흔히 쓰는 방법은 이름과 점수를 리스트 두 개에 따로 두는 것입니다.
names = ["학생 A", "학생 B", "학생 C"]
scores = [90, 85, 72]
# 「학생 B의 점수」를 알려면 이름의 인덱스부터 찾아야 한다
i = names.index("학생 B")
print(scores[i]) # 85
# 두 리스트의 「순서가 어긋나면」 조용히 엉뚱한 학생의 점수가 나온다index로 이름의 인덱스를 찾고, 그 인덱스로 다른 리스트에서 점수를 꺼냅니다. 두 리스트의 순서가 언제나 같아야 이 방법이 성립합니다.
어긋나도 오류가 나지 않습니다
한쪽만 정렬하거나 한쪽에만 값을 더하면 인덱스가 밀립니다. 그래도 프로그램은 멈추지 않고 엉뚱한 학생의 점수를 그대로 꺼내 옵니다. 프로그램이 멈추지 않는 오류는 찾기가 특히 어렵습니다.
스스로 확인
리스트 두 개를 쓰는 방법에서 학생 한 명을 가운데 삽입하려면 무엇을 해야 할까요?
키와 값을 쌍으로 저장하기
인덱스 대신 키로 꺼냅니다.
값마다 찾을 때 쓸 키를 붙여 저장해 두면 두 리스트의 순서를 맞출 필요가 없어집니다.
딕셔너리(dictionary)
키(key)와 값(value)을 쌍으로 묶어 저장하는 자료형입니다. 키와 값 한 쌍을 항목(item)이라고 합니다. 리스트가 인덱스로 값을 꺼낸다면, 딕셔너리는 키로 꺼냅니다. 그래서 항목의 순서가 바뀌어도 값을 꺼내는 데는 영향이 없습니다.
scores = {"학생 A": 90, "학생 B": 85, "학생 C": 72}
print(scores["학생 B"]) # 85 인덱스가 아니라 「키」로 꺼낸다
print(len(scores)) # 3
scores["학생 D"] = 68 # 없던 키면 「새 항목이 추가된다」
scores["학생 A"] = 95 # 있던 키면 「값이 바뀐다」
print(scores) # {'학생 A': 95, '학생 B': 85, '학생 C': 72, '학생 D': 68}
del scores["학생 D"] # 키와 값을 함께 삭제한다
print(len(scores)) # 3| 리스트 | 딕셔너리 | |
|---|---|---|
| 만드는 법 | [90, 85] |
{"A": 90, "B": 85} |
| 꺼내기 | a[0] — 인덱스 |
d["A"] — 키 |
| 추가하기 | append로 끝에 |
d["C"] = 72 |
| 없는 것을 꺼내면 | IndexError |
KeyError |
| 어울리는 것 | 순서가 의미를 갖는 값 | 이름(키)으로 찾는 값 |
대괄호 한 줄이 두 가지 일을 합니다
d["학생 D"] = 68은 없던 키면 항목을 새로 추가하고, 있던 키면 값을 바꿉니다. 리스트의 append와 a[0] = …이 딕셔너리에서는 한 문법입니다. 편리한 대신, 키를 잘못 적으면 기존 값이 바뀌지 않고 새 항목이 추가됩니다.
스스로 확인
scores["학생 A"] = 95를 두 번 실행하면 값이 두 번 더해질까요, 그대로일까요?
없는 키를 꺼낼 때
오류로 멈출지, 먼저 확인할지, 기본값을 쓸지 고릅니다.
리스트에서 범위를 벗어난 인덱스로 값을 꺼내면 오류가 나듯, 딕셔너리도 없는 키로 값을 꺼내면 오류가 나고 프로그램이 멈춥니다.
scores = {"학생 A": 90, "학생 B": 85}
# print(scores["학생 Z"]) # KeyError. 없는 키를 꺼내면 멈춘다
print("학생 Z" in scores) # False 먼저 있는지 확인하거나
print(scores.get("학생 Z")) # None get으로 꺼내거나
print(scores.get("학생 Z", 0)) # 0 없을 때 쓸 기본값을 지정하거나세 가지 모두 쓰입니다. 어느 하나가 옳다기보다, 키가 없는 상황이 있어서는 안 되는 일인지 있을 수 있는 일인지에 따라 고릅니다.
| 쓰는 법 | 없을 때 | 이럴 때 쓴다 |
|---|---|---|
d[k] |
KeyError로 멈춘다 |
없으면 프로그램이 잘못된 것일 때 |
k in d |
False |
있는지 없는지에 따라 처리를 나눠야 할 때 |
d.get(k) |
None |
키가 없어도 그냥 넘어가도 될 때 |
d.get(k, 0) |
지정한 기본값 | 없으면 0부터 시작하고 싶을 때 |
get이 늘 나은 것은 아닙니다
get은 키가 없어도 멈추지 않으므로 오타를 감춥니다. d.get("학셍 A")는 오류 없이 None을 반환합니다. 있어야 마땅한 키라면 오히려 멈추는 쪽이 낫습니다.
스스로 확인
d.get(k)가 None을 반환했습니다. 키가 없는 것일까요, 값이 None인 것일까요? 어떻게 구별할 수 있을까요?
순회하며 개수 세기
딕셔너리를 가장 자주 쓰는 일 가운데 하나입니다.
딕셔너리도 for 문으로 순회합니다. 다만 반복할 때마다 무엇이 나오는지를 알아야 합니다.
scores = {"학생 A": 90, "학생 B": 85, "학생 C": 72}
for name in scores: # 그냥 순회하면 「키」가 나온다
print(name, scores[name]) # 학생 A 90 / 학생 B 85 / 학생 C 72
for name, score in scores.items(): # 키와 값을 「쌍」으로 받는다
print(name, score) # 위와 같다
print(list(scores.keys())) # ['학생 A', '학생 B', '학생 C']
print(list(scores.values())) # [90, 85, 72]
print(sum(scores.values()) / len(scores)) # 82.33333333333333그냥 순회하면 키가 나옵니다
for name in scores:에서는 값이 아니라 키가 하나씩 name에 대입됩니다. 값까지 함께 쓰려면 .items()로 (키, 값) 쌍을 받습니다. 그 쌍이 튜플이라서 for name, score in …처럼 받으면서 언패킹할 수 있습니다. 파이썬 3.7부터 딕셔너리는 항목을 넣은 순서를 지키므로, 키도 넣은 순서대로 나옵니다. 다만 값을 꺼낼 때 쓰는 것은 순서가 아니라 키입니다.
| 쓰는 법 | 나오는 것 |
|---|---|
for k in d |
키 |
d.keys() |
키 전부 |
d.values() |
값 전부 — sum·max를 바로 쓸 수 있다 |
d.items() |
(키, 값) 쌍 |
무엇이 몇 번 나왔는지 세는 일은 딕셔너리의 대표적인 쓰임입니다.
votes = ["사과", "포도", "사과", "바나나", "사과", "포도"]
count = {}
for fruit in votes:
if fruit in count:
count[fruit] = count[fruit] + 1
else:
count[fruit] = 1 # 처음 나온 것이면 1로 시작한다
print(count) # {'사과': 3, '포도': 2, '바나나': 1}처음 나온 과일이면 새 항목을 추가해 1로 시작하고, 이미 있으면 값에 1을 더합니다. if로 경우를 나누어 작성한 이 코드는 get을 쓰면 한 줄로 줄어듭니다.
votes = ["사과", "포도", "사과", "바나나", "사과", "포도"]
count = {}
for fruit in votes:
count[fruit] = count.get(fruit, 0) + 1 # 없으면 0에서 시작한다
print(count) # {'사과': 3, '포도': 2, '바나나': 1}count.get(fruit, 0)은 「있으면 그 값, 없으면 0」입니다. 그래서 처음 나온 과일도 0 + 1이 되어 1부터 셉니다.
스스로 확인
count.get(fruit, 0)에서 기본값을 0이 아니라 1로 두면 결과가 어떻게 달라질까요?
키가 될 수 있는 값
아무 값이나 키가 되지는 않습니다.
키에는 조건이 하나 있습니다.
ok = {("서울", "부산"): 400} # 튜플은 키가 된다 — 바뀌지 않으니까
print(ok[("서울", "부산")]) # 400
# bad = {["서울", "부산"]: 400} # TypeError. 리스트는 키가 될 수 없다
# 리스트는 나중에 바뀔 수 있어 「값을 저장한 위치」를 다시 찾을 수 없다왜 바뀌면 안 되나
딕셔너리는 키로 값을 저장할 위치를 계산합니다. 저장한 뒤에 키가 바뀌면 그 위치를 다시 찾을 수 없습니다. 그래서 파이썬은 바뀔 수 있는 값은 아예 키로 쓸 수 없게 막습니다.
리스트는 키가 될 수 없지만 튜플은 됩니다. 「서울에서 부산까지」처럼 값 두 개가 함께 하나의 키가 되는 경우에 씁니다. 다만 튜플 안에 리스트가 들어 있으면 그 리스트가 바뀔 수 있으므로 키가 되지 못합니다.
배운 것을 모으면 파일에서 읽은 내용을 딕셔너리에 저장하는 프로그램이 됩니다.
# 성적.csv 한 줄은 「이름,국어,영어」다
table = {}
with open("성적.csv", "r", encoding="utf-8") as f:
for line in f:
values = line.strip().split(",")
name = values[0]
table[name] = (int(values[1]), int(values[2])) # 점수 둘을 튜플로 묶어 저장한다
for name, (kor, eng) in table.items():
print(f"{name}: 국어 {kor}, 영어 {eng}, 평균 {(kor + eng) / 2}")
# 학생 A: 국어 90, 영어 65, 평균 77.5
# 학생 B: 국어 85, 영어 80, 평균 82.5
print(table.get("학생 Z", "없는 학생")) # 없는 학생이름을 키로, 점수 둘을 묶은 튜플을 값으로 저장했습니다. .items()가 만드는 (키, 값) 쌍을 name, (kor, eng)로 받아 안쪽 튜플까지 한 번에 언패킹했습니다.
스스로 확인
값으로 튜플 대신 리스트를 저장하면 무엇이 달라질까요? 리스트를 키가 아니라 값으로 쓴다는 점에 주의해 생각해 보세요.
핵심 정리
인덱스가 아니라 키로
리스트는 인덱스로, 딕셔너리는 키로 값을 꺼낸다. 그래서 항목의 순서가 바뀌어도 값을 꺼내는 데는 영향이 없다.
대괄호 한 줄이 두 가지 일을 한다
d[k] = v는 없던 키면 항목을 추가하고 있던 키면 값을 바꾼다. 키를 잘못 적으면 기존 값이 바뀌지 않고 새 항목이 추가된다.
없는 키를 다루는 세 가지 방법
d[k]는 오류로 멈추고, k in d는 먼저 확인하고, d.get(k, 0)은 지정한 기본값을 반환한다. 있어야 마땅한 키라면 멈추는 쪽이 낫다.
순회하면 키가 나온다
값까지 쓰려면 .items()로 (키, 값) 쌍을 받는다. 그 쌍이 튜플이라서 받으면서 언패킹할 수 있다. 파이썬 3.7부터 키는 넣은 순서대로 나온다.
키는 바뀌지 않는 값이라야 한다
문자열 · 숫자, 그리고 그런 값으로만 이루어진 튜플은 키가 되고 리스트는 되지 않는다. 키가 바뀌면 값을 저장한 위치를 다시 찾을 수 없기 때문이다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. d = {"A": 1}일 때 d["B"] = 2를 실행하면?
2. 딕셔너리 scores에 대해 for x in scores:에서 x에 들어오는 것은?
3. 다음 중 딕셔너리의 키로 쓸 수 없는 것은?