PYTHON

파이썬 딕셔너리는 번호가 아니라 이름표로 꺼냅니다 — 없는 키는 KeyError

PANCO_IT
#파이썬#python#딕셔너리#dict#KeyError#get#리스트#키와값#파이썬기초#파이썬독학

나무 서랍장에 종이 라벨이 붙어 있고 라벨에는 한 단어가 인쇄되어 있다

이미지 출처: Pexels · Polina Zimmerman · Pexels License

이름으로 점수를 찾는 코드입니다. 두 가지로 짰습니다. 둘 다 92를 출력합니다.

A. 리스트 두 개

names = ["홍길동", "김철수", "이영희"]
scores = [88, 92, 75]

i = names.index("김철수")
print(scores[i])

B. 딕셔너리 한 개

scores = {"홍길동": 88, "김철수": 92, "이영희": 75}

print(scores["김철수"])

A는 5줄, B는 3줄입니다. 줄 수 이야기를 하려는 게 아닙니다. 두 코드는 값을 찾는 방식이 다르고, 그 차이가 나중에 어디서 터지는지도 다릅니다.

세 라운드로 견주겠습니다.

대결 구도 — 번호로 찾느냐, 이름표로 찾느냐

리스트는 번호로 찾습니다. scores[1]1은 두 번째 칸이라는 뜻입니다. 값 자체와는 아무 상관이 없습니다.

딕셔너리는 이름표로 찾습니다. scores["김철수"]"김철수"는 몇 번째가 아니라 그 값에 붙여 둔 이름입니다.

번호로 찾는 리스트와 이름표로 찾는 딕셔너리를 나란히 그린 그림. 왼쪽은 0번부터 2번까지 번호가 붙은 사물함 세 칸에 88, 92, 75가 들어 있고 이름을 알아도 몇 번 칸인지 먼저 알아내야 한다는 표시가 있다. 오른쪽은 홍길동, 김철수, 이영희 이름표가 붙은 서랍 세 개에 같은 값이 들어 있고 이름표를 그대로 대면 열린다는 표시가 있다

왼쪽은 번호를 알아야 열리고, 오른쪽은 이름표를 그대로 대면 열립니다.

이름표를 붙이는 문법은 중괄호입니다.

scores = {"홍길동": 88, "김철수": 92, "이영희": 75}

이름표: 값 쌍을 쉼표로 잇고 {}로 감쌉니다. 이름표를 키(key), 담긴 것을 **값(value)**이라고 부릅니다.

딕셔너리 한 개의 구조를 해부한 그림. 중괄호 안에 키와 값이 콜론으로 짝지어 세 쌍 들어 있고, 키 쪽에는 같은 키를 두 번 쓸 수 없고 문자열이나 숫자처럼 바뀌지 않는 것만 쓸 수 있다는 설명이, 값 쪽에는 숫자든 문자열이든 리스트든 아무거나 들어가고 중복돼도 된다는 설명이 붙어 있다

키는 유일하고 바뀌지 않는 것만 쓸 수 있습니다. 값에는 제한이 없습니다.

키에 조건이 있는 이유는 서랍 이름표를 떠올리면 됩니다. 같은 이름표가 두 개 붙어 있으면 어느 서랍인지 알 수 없고, 이름표가 계속 바뀌면 찾을 수 없습니다. 그래서 리스트는 키가 될 수 없습니다. 리스트는 내용이 바뀌니까요.

d = {["a", "b"]: 1}
TypeError: unhashable type: 'list'

라운드 1 — 꺼내기

A는 두 단계입니다. 이름이 몇 번째인지 먼저 찾고(names.index), 그 번호로 점수를 꺼냅니다(scores[i]). B는 한 단계입니다.

단계 수보다 중요한 것은 A가 두 리스트의 순서가 맞다는 것을 전제한다는 점입니다. 그 전제가 깨지면 어떻게 되는지 보겠습니다.

한 명을 지웠더니 점수가 밀렸습니다
names = ["홍길동", "김철수", "이영희"]
scores = [88, 92, 75]

names.remove("홍길동")

i = names.index("김철수")
print("김철수의 점수는", scores[i])

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

88이 나옵니다. 김철수의 점수는 92인데요.

names에서 홍길동을 지웠더니 김철수가 0번으로 당겨졌고, scores는 그대로라 0번이 여전히 홍길동의 88입니다. 에러는 나지 않습니다. 점수가 있고 번호도 유효하니 파이썬이 막을 이유가 없습니다.

딕셔너리는 이름표와 값이 한 몸이라 이런 어긋남이 생기지 않습니다.

딕셔너리는 지워도 짝이 유지됩니다
scores = {"홍길동": 88, "김철수": 92, "이영희": 75}

del scores["홍길동"]

print("김철수의 점수는", scores["김철수"])
print(scores)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

라운드 1은 딕셔너리입니다. 리스트 두 개를 나란히 두는 구조는 한쪽만 고치는 순간 조용히 틀립니다.

라운드 2 — 고치고 더하기

새 사람을 넣어 보겠습니다.

A는 두 줄이고 순서를 맞춰야 합니다.

names.append("박민수")
scores.append(60)

B는 한 줄입니다.

scores["박민수"] = 60

값을 고치는 것도 같은 문법입니다. 없으면 새로 만들고 있으면 덮어씁니다.

같은 줄이 추가도 하고 수정도 합니다
scores = {"홍길동": 88, "김철수": 92}

scores["박민수"] = 60
print(scores)

scores["박민수"] = 100
print(scores)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

여기에 함정이 하나 있습니다. 덮어쓸 때도 아무 말이 없습니다. 이미 있는 키인 줄 모르고 다시 넣으면 앞의 값이 사라지는데, 에러도 경고도 나지 않습니다.

키가 중복될 수 없다는 성질은 만들 때도 똑같이 작동합니다.

scores = {"홍길동": 88, "홍길동": 99}
print(scores)
{'홍길동': 99}

같은 이름표 두 개를 붙인 게 아니라 뒤엣것이 앞엣것을 덮었습니다.

라운드 2도 딕셔너리입니다. 다만 덮어쓰기가 조용하다는 점은 기억해 둘 값입니다.

라운드 3 — 없는 것을 물었을 때

여기서는 딕셔너리가 한 대 맞습니다.

없는 키를 물으면 멈춥니다
scores = {"홍길동": 88, "김철수": 92}

print(scores["박민수"])

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

KeyError: '박민수'

프로그램이 그 자리에서 멈춥니다. 뒤에 줄이 더 있어도 실행되지 않습니다.

KeyError 메시지 한 줄을 확대해 각 부분을 짚은 그림. 왼쪽에는 코드 세 줄이 축소되어 있고 세 번째 줄에서 화살표가 나와 오른쪽 확대 상자로 이어진다. 확대 상자에는 KeyError 라는 오류 이름과 작은따옴표에 싸인 박민수 라는 키 이름이 있고, 오류 이름은 딕셔너리에 없는 키를 물었다는 뜻이고 따옴표 안이 내가 물어본 이름표라는 설명이 각각 붙어 있다

오류 이름이 무엇이 잘못됐는지를, 따옴표 안이 어느 이름표인지를 알려 줍니다.

리스트도 없는 것을 물으면 멈춥니다. 다만 이름이 다릅니다.

자료형없는 것을 물으면오류 이름
리스트scores[99]IndexError
리스트names.index("없는이름")ValueError
딕셔너리scores["없는이름"]KeyError

멈추는 게 싫다면 get()이 있습니다.

get은 멈추지 않습니다
scores = {"홍길동": 88, "김철수": 92}

print(scores.get("박민수"))
print(scores.get("박민수", 0))
print("박민수" in scores)

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

  • get(키) — 없으면 None을 돌려줍니다. 멈추지 않습니다.
  • get(키, 기본값) — 없으면 그 기본값을 돌려줍니다.
  • 키 in 딕셔너리 — 있는지 먼저 물어봅니다. True 또는 False를 돌려줍니다.

그런데 get()이 항상 더 안전한 것은 아닙니다. 없는 키를 물었는데 None이 돌아오고 그걸 계속 쓰면, 문제가 실수한 줄이 아니라 한참 뒤에서 터집니다.

score = scores.get("박민수")
print(score + 10)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

지난 편의 email.lower 실수와 같은 모양입니다. 조용히 넘어간 값은 나중에 더 찾기 어려운 자리에서 터집니다.

라운드 3은 리스트의 판정승도 딕셔너리의 판정승도 아닙니다. 둘 다 멈추고, 다른 점은 오류 이름뿐입니다. 여기서 갈리는 것은 자료형이 아니라 내가 get을 쓸지 in으로 먼저 확인할지입니다.

판정

관점리스트 두 개딕셔너리판정
꺼내기index()로 번호를 먼저 찾는다이름표로 바로딕셔너리
짝 유지한쪽만 고치면 조용히 어긋난다이름표와 값이 한 몸딕셔너리
더하기·고치기두 리스트에 각각한 줄로 둘 다딕셔너리
없는 것IndexError / ValueErrorKeyError무승부
순서가 뜻을 가질 때1등·2등·대기 순번순서로 꺼내는 도구가 아니다리스트
같은 종류의 나열점수만 모으기, 이름만 모으기굳이 키를 지어내야 한다리스트

언제 무엇을 쓰나

한 문장으로 가릅니다. 무엇으로 찾을 것인지를 보면 됩니다.

  • 순서로 찾는다면 리스트입니다. 대기 1번·2번·3번이나 1등부터 10등까지처럼, 몇 번째인지가 뜻을 가질 때입니다.
  • 이름으로 찾는다면 딕셔너리입니다. 이름으로 점수를, 상품코드로 가격을, 도시로 인구를 찾을 때처럼 몇 번째인지가 아무 뜻이 없을 때입니다.

리스트 두 개를 나란히 두고 순서를 맞춰 쓰고 있다면, 그건 대개 딕셔너리로 바꿔야 할 신호입니다.

한 가지 덧붙이면, 요즘 파이썬의 딕셔너리는 넣은 순서를 기억합니다. 그래서 출력하면 넣은 차례대로 나옵니다. 다만 그건 보여 줄 때의 이야기이고, 값을 꺼내는 수단은 여전히 번호가 아니라 키입니다. scores[0]은 0번 칸이 아니라 키가 0인 항목을 찾습니다.

딕셔너리 문법 일곱 줄

하는 일문법
만들기{키: 값, ...}{"a": 1, "b": 2}
빈 것 만들기{}d = {}
꺼내기d[키]d["a"]1
안전하게 꺼내기d.get(키, 기본값)d.get("z", 0)0
넣기·고치기d[키] = 값d["c"] = 3
지우기del d[키]del d["a"]
있는지키 in d"a" in dTrue
개수len(d)len(d)2

순회는 짝으로 돕니다.

키와 값을 짝으로 돌기
scores = {"홍길동": 88, "김철수": 92, "이영희": 75}

for name in scores:
  print(name)

print("---")

for name, score in scores.items():
  print(name, score)

print("---")
print(sum(scores.values()) / len(scores))

📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.

  • for name in scores: — 그냥 돌면 키만 나옵니다. 값이 아닙니다.
  • for name, score in scores.items(): — 키와 값을 짝으로 받습니다.
  • scores.values() — 값만 모읍니다. 평균 같은 계산에 씁니다. 키만 필요하면 scores.keys()입니다.

첫 번째가 헷갈리기 쉬운 자리입니다. 리스트를 돌면 값이 나오는데 딕셔너리를 돌면 키가 나옵니다.

한 줄만 바꾸면 결과는 어떻게 될까요

미니 퀴즈

OX 3문제 + 4지선다 5문제

참고 자료


포켓코딩(PYTHON2) 2편 · 1편(파이썬 문자열 메서드)에서 이어집니다. 예제 코드는 강사 개인 GitHub 저장소의 강의 자료를 바탕으로 다시 썼습니다. 다음 편은 튜플과 집합입니다. 괄호만 바꿨는데 왜 고칠 수 없게 되는지, 중복을 없애는 데 왜 집합이 가장 짧은지를 다룹니다.

PANCO_IT

IT 지식을 정리하는 개발자

관련 글