Python 강좌 : 제 12강 - Dictionary 기초 연산
사전(Dictionary)
Python에서는 중괄호({})를 이용하여 사전을 생성할 수 있습니다. 사전은 매칭에 사용되며 key와 value로 구성 되어있습니다.
key를 호출하여 value를 불러옵니다. key는 중복이 불가능하며 value는 중복이 가능합니다.
Python 3.7 이상부터 사전은 요소를 추가한 순서(삽입 순서)를 유지하는 것이 언어 차원에서 보장됩니다. 단, 목록처럼 index로 접근하지는 않으며 key로만 접근합니다.
데이터베이스처럼 key와 value로 구성되어 있고, value의 값으로 dict나 list를 사용할 수도 있습니다.
key에는 문자열, 숫자, 튜플처럼 변경할 수 없는(Immutable) 값, 정확히는 해시 가능한(Hashable) 값만 사용할 수 있으며, 목록을 key로 사용하면 TypeError가 발생합니다.
목록과 튜플은 원소를 0, 1, 2처럼 위치 번호로 찾았습니다. 하지만 “홍길동의 전화번호”처럼 이름표로 값을 찾는 데이터는 위치 번호로 관리하기 어렵습니다. 사전은 "홍길동" 같은 key를 이름표로 붙여 value를 저장하고, 이름표로 바로 값을 꺼냅니다. 실제 국어사전에서 단어(key)로 뜻(value)을 찾는 것과 같습니다.
제 8강에서 사용한 네 가지 기준으로 사전을 정리하면 다음과 같습니다.
- 변경 가능(Mutable) : 항목을 추가·수정·삭제할 수 있습니다.
- 삽입 순서 유지 : Python 3.7 이상에서 넣은 순서대로 반복·출력됩니다. 단, 위치 번호로 접근하지는 않습니다.
- key 중복 불가, value 중복 허용 : 같은
key로 다시 넣으면 새 항목이 생기지 않고 기존value가 바뀝니다. - 해시 기반 :
key의 해시값으로 저장 위치를 정합니다.
사전이 빠른 이유는 해시(Hash)에 있습니다. 사전은 key를 저장할 때 hash(key)로 정수를 계산하고, 그 정수로 저장할 칸의 위치를 정합니다. 나중에 같은 key로 값을 찾을 때도 해시값을 다시 계산해 해당 칸으로 바로 이동하므로, 목록처럼 처음부터 하나씩 비교할 필요가 없습니다.
그래서 사전의 조회·추가·삭제와 key in 사전 검사는 항목이 10개든 100만 개든 평균적으로 거의 일정한 시간에 끝납니다. (서로 다른 key의 해시값이 겹치는 경우가 있어 “평균적으로”라고 표현합니다.)
이 방식 때문에 key는 해시값을 계산할 수 있고, 그 값이 바뀌지 않는 객체여야 합니다. 목록처럼 내용이 바뀌는 객체를 key로 쓰면 저장한 뒤 내용이 바뀌어 해시값이 달라질 수 있으므로, Python은 처음부터 이를 허용하지 않습니다. value에는 이런 제한이 없습니다.
이번 강좌에서는 사전의 생성, 접근, 추가, 수정, 병합, 삭제, 조사, 순서를 다룹니다.
생성
사전은 key: value 쌍을 쉼표(,)로 구분하여 중괄호({})로 감싸서 만듭니다. key와 value 사이에는 콜론(:)을 씁니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
b = dict.fromkeys(["one", "two", "three"], "알 수 없음")
print(a)
print(b)- 결과
- {‘one’: ‘하나’, ‘two’: ‘둘’, ‘three’: ‘셋’}
{‘one’: ‘알 수 없음’, ‘two’: ‘알 수 없음’, ‘three’: ‘알 수 없음’}
사전은 { key1:value1, key2:value2, ... }의 형태로 생성할 수 있습니다.
또는 dict.fromkeys(key, default)를 통하여 각각의 key에 default의 값을 지니는 사전을 생성할 수 있습니다.
dict.fromkeys()는 key 목록은 정해져 있지만 값은 아직 모를 때, 모든 key를 같은 초깃값으로 준비해 두는 용도로 사용합니다. default를 생략하면 모든 값이 None이 됩니다.
a = dict(one=1, two=2)
b = dict([("one", 1), ("two", 2)])
c = dict(zip(["one", "two"], [1, 2]))
print(a)
print(b)
print(c)- 결과
- {‘one’: 1, ‘two’: 2}
{‘one’: 1, ‘two’: 2}
{‘one’: 1, ‘two’: 2}
dict()를 이용해서도 사전을 만들 수 있으며, 세 가지 방법 모두 같은 사전을 만듭니다.
dict(one=1, two=2)처럼 이름=값 형태로 전달하면 이름이 문자열 key가 됩니다. 단, 이 방식은 key가 변수 이름으로 쓸 수 있는 문자열일 때만 가능합니다.
dict([("one", 1), ("two", 2)])처럼 (key, value) 튜플의 목록을 전달할 수도 있습니다.
zip(key목록, value목록)은 두 목록에서 원소를 하나씩 짝지어 (key, value) 쌍을 만들어 주므로, 따로 있는 두 목록을 사전으로 합칠 때 편리합니다.
a = {"a": 1, "a": 2}
b = {1: "정수", True: "논리", 1.0: "실수"}
print(a)
print(b)
try:
{[1, 2]: "목록"}
except TypeError as e:
print(e)- 결과
- {‘a’: 2}
{1: ‘실수’}
cannot use ‘list’ as a dict key (unhashable type: ‘list’)
key는 중복될 수 없으므로, 같은 key를 두 번 쓰면 나중에 쓴 value만 남습니다. 오류가 발생하지 않으므로 실수로 같은 key를 쓰지 않도록 주의합니다.
1, True, 1.0은 자료형이 다르지만 1 == True == 1.0이고 해시값도 같으므로, 사전은 이들을 같은 key로 취급합니다. 그래서 key는 처음 넣은 1로 남고, value는 마지막에 넣은 '실수'로 덮어써집니다.
목록을 key로 사용하면 TypeError가 발생합니다. Python 3.14에서는 오류 메시지가 cannot use 'list' as a dict key (unhashable type: 'list')처럼 원인을 더 자세히 알려 줍니다. 여러 값을 묶어 key로 쓰려면 목록 대신 제 11강의 튜플을 사용합니다.
a = dict.fromkeys(["x", "y"], [])
a["x"].append(1)
print(a)- 결과
- {‘x’: [1], ‘y’: [1]}
dict.fromkeys()의 default에 목록처럼 변경 가능한 객체를 전달하면, 모든 key가 같은 목록 하나를 공유합니다. 그래서 a["x"]에만 1을 추가했는데 a["y"]에도 1이 보입니다.
8강에서 본 [[0] * 3] * 3 문제와 같은 원리입니다. key마다 별도의 목록이 필요하다면 아래 추가 절의 setdefault()를 사용하거나 반복문으로 key마다 새 목록을 만들어 넣습니다.
접근
사전에서 값을 꺼낼 때는 위치 번호 대신 key를 사용합니다. key가 반드시 있다고 확신할 때는 대괄호를, 없을 수도 있을 때는 get()을 사용합니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
print(a["one"])
print(a.get("two"))
print(a.get("four", "넷"))- 결과
- 하나
둘
넷
사전의 접근 방식은 대괄호([])에 index대신에 key를 호출하여 value를 출력합니다.
또는 사전.get(key)를 통해서 value를 호출할 수 있습니다.
사전.get(key, default)처럼 두 번째 인수를 전달하면, 사전에 key가 존재하지 않을 때 default 값을 대신 반환합니다.
a.get("four", "넷")은 "four"가 사전에 없으므로 "넷"을 반환했을 뿐, 사전에 "four"를 추가하지는 않습니다. get()은 사전을 변경하지 않는 조회 전용 메서드입니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
print(a.get("four"))
try:
print(a["four"])
except KeyError as e:
print("KeyError :", e)- 결과
- None
KeyError : ‘four’
존재하지 않는 key를 사전[key]로 호출하면 KeyError가 발생합니다.
반면 사전.get(key)는 오류 대신 None을 반환하므로, key가 없을 수도 있는 경우에 안전하게 사용할 수 있습니다.
- Tip :
key가 반드시 있어야 하는 상황이라면 오히려사전[key]를 사용하는 것이 좋습니다. 잘못된key를 쓰면 즉시 오류가 나서 실수를 빨리 발견할 수 있기 때문입니다.get()은 “없을 수도 있다”는 것이 정상적인 경우에 사용합니다.
words = ["사과", "배", "사과", "포도", "사과"]
count = {}
for w in words:
count[w] = count.get(w, 0) + 1
print(count)- 결과
- {‘사과’: 3, ‘배’: 1, ‘포도’: 1}
get()의 기본값은 개수 세기에 특히 유용합니다.
처음 나온 단어는 사전에 없으므로 count.get(w, 0)이 0을 반환하고, 여기에 1을 더해 저장합니다. 이미 있는 단어는 저장된 개수에 1을 더합니다. 결과적으로 사과는 3번, 배와 포도는 1번 나온 것을 확인할 수 있으며, key는 처음 등장한 순서대로 저장됩니다.
추가
사전에 새 항목을 넣는 방법은 대괄호 대입, setdefault(), update() 세 가지입니다. 목록의 append()처럼 “맨 뒤에 추가”라는 개념 대신, key를 정해서 넣습니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
a["four"] = "넷"
# a.setdefault("four", "넷")
print(a["four"])
a.update({"five": "다섯", "six": "여섯"})
print(a)- 결과
- 넷
{‘one’: ‘하나’, ‘two’: ‘둘’, ‘three’: ‘셋’, ‘four’: ‘넷’, ‘five’: ‘다섯’, ‘six’: ‘여섯’}
사전[key]=value를 이용하여 요소를 추가할 수 있습니다.
사전.setdefault(key, value)를 이용하여 요소를 추가할 수 있습니다.
사전.update({'key':'value', 'key':'value'})를 이용하여 다수의 요소를 추가할 수 있습니다.
새로 추가한 four, five, six가 기존 항목 뒤에 추가한 순서대로 출력되는 것을 확인할 수 있습니다.
- Tip :
setdefault()는 초깃값을 설정하는 함수이므로, 이미 key가 존재하는 경우 value는 수정되지 않습니다.
a = {"one": "하나"}
print(a.setdefault("one", "일"))
print(a.setdefault("two", "둘"))
print(a)- 결과
- 하나
둘
{‘one’: ‘하나’, ‘two’: ‘둘’}
setdefault(key, value)는 key가 없을 때만 값을 넣고, 어느 경우든 그 key의 현재 value를 반환합니다.
"one"은 이미 있으므로 값을 바꾸지 않고 기존 값 "하나"를 반환했고, "two"는 없으므로 "둘"을 넣은 뒤 그 값을 반환했습니다.
groups = {}
for name in ["kim", "lee", "kang", "lim"]:
groups.setdefault(name[0], []).append(name)
print(groups)- 결과
- {‘k’: [‘kim’, ‘kang’], ‘l’: [‘lee’, ‘lim’]}
setdefault()가 값을 반환한다는 점을 이용하면, 같은 기준끼리 목록으로 묶는 코드를 짧게 작성할 수 있습니다.
name[0](첫 글자)이 처음 나오면 빈 목록을 새로 만들어 넣고 그 목록을 반환하며, 이미 있으면 기존 목록을 반환합니다. 반환된 목록에 append()로 이름을 추가하므로, 첫 글자가 같은 이름끼리 모입니다. dict.fromkeys(키목록, [])와 달리 key마다 서로 다른 목록이 만들어집니다.
수정
사전에서 수정과 추가는 같은 문법을 사용합니다. key가 이미 있으면 value를 바꾸고, 없으면 새 항목을 만듭니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
a["three"] = "둘"
a.update(two="셋")
print(a)- 결과
- {‘one’: ‘하나’, ‘two’: ‘셋’, ‘three’: ‘둘’}
사전[key]=value를 이용하여 value 값을 수정 할 수 있습니다.
사전.update(key=value)를 사용하여 기존에 존재하는 key에 대한 value 값을 수정할 수 있습니다.
"three"의 값이 "둘"로, "two"의 값이 "셋"으로 바뀌었습니다. 값이 바뀌어도 key의 순서는 처음 추가했을 때의 위치를 유지합니다. value는 중복이 허용되므로 서로 다른 key가 같은 값을 가져도 문제없습니다.
a = {"one": "하나"}
a["one"] = "일"
print(a, len(a))- 결과
- {‘one’: ‘일’} 1
같은 key에 다시 대입하면 항목이 하나 더 생기는 것이 아니라 기존 값이 덮어써집니다. 그래서 길이는 여전히 1입니다.
이 동작은 편리하지만, 기존 값을 실수로 덮어쓸 위험도 있습니다. 이미 있는 값은 건드리지 않고 없을 때만 넣고 싶다면 setdefault()를 사용합니다.
병합
두 사전을 하나로 합치는 방법에는 원본을 바꾸는 update()와, 새 사전을 만드는 | 연산자가 있습니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
b = {"five": "다섯", "six": "여섯"}
a.update(b)
print(a)
print(b)- 결과
- {‘one’: ‘하나’, ‘two’: ‘둘’, ‘three’: ‘셋’, ‘five’: ‘다섯’, ‘six’: ‘여섯’}
{‘five’: ‘다섯’, ‘six’: ‘여섯’}
사전.update(사전)을 이용하여 서로 다른 사전을 병합할 수 있습니다.
update()는 원본 사전 a를 직접 변경하며, 인수로 전달한 사전 b는 변경되지 않습니다.
a = {"one": "하나", "two": "둘"}
b = {"two": "이", "three": "삼"}
c = a | b
print(c)
print(a)
a |= b
print(a)- 결과
- {‘one’: ‘하나’, ‘two’: ‘이’, ‘three’: ‘삼’}
{‘one’: ‘하나’, ‘two’: ‘둘’}
{‘one’: ‘하나’, ‘two’: ‘이’, ‘three’: ‘삼’}
Python 3.9 이상에서는 | 연산자와 |= 연산자로 사전을 병합할 수 있습니다.
사전1 | 사전2는 두 사전을 병합한 새로운 사전을 반환하며, 원본 사전은 변경되지 않습니다.
사전1 |= 사전2는 사전1.update(사전2)와 같이 사전1을 직접 변경합니다.
- Tip : 두 사전에 같은
key가 있다면 오른쪽 사전의 value가 남습니다. 예제에서"two"의 값이"이"로 바뀐 것을 확인할 수 있습니다.
a = {"one": "하나"}
print(a.update(two="둘"))
a.update([("three", "셋")])
print(a)
try:
a | [("four", "넷")]
except TypeError as e:
print(e)- 결과
- None
{‘one’: ‘하나’, ‘two’: ‘둘’, ‘three’: ‘셋’}
unsupported operand type(s) for |: ‘dict’ and ‘list’
update()는 원본을 바꾸는 메서드이므로 목록의 append()처럼 None을 반환합니다. a = a.update(...)처럼 작성하면 사전을 잃어버리므로 주의합니다.
update()는 사전뿐 아니라 (key, value) 쌍의 목록도 받을 수 있지만, | 연산자는 양쪽이 모두 사전이어야 하며 목록을 전달하면 TypeError가 발생합니다.
- Tip : 원본을 보존하면서 합친 결과가 필요하면
|, 원본에 바로 반영하려면update()나|=를 사용합니다.
삭제
항목을 지우는 방법은 del, pop(), popitem(), clear()가 있습니다. 지운 값을 다시 사용해야 하는지에 따라 선택합니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
del a["three"]
# a.pop("three")
# a.popitem()
print(a)
a.clear()
print(a)- 결과
- {‘one’: ‘하나’, ‘two’: ‘둘’}
{}
del 사전['key']을 이용하여 사전의 특정 요소를 삭제할 수 있습니다.
사전.pop('key')을 이용하여 del처럼 사전의 특정 요소를 삭제할 수 있습니다. 이때 삭제한 요소의 value를 반환합니다.
사전.popitem()을 이용하여 가장 마지막에 추가된 요소를 삭제할 수 있습니다. 이때 삭제한 요소를 (key, value) 튜플로 반환합니다.
사전.clear()을 이용하여 사전의 모든 값을 삭제합니다.
del과 pop()도 없는 key를 지정하면 KeyError가 발생합니다.
-
Tip :
popitem()이 마지막에 추가된 요소를 꺼내는 동작은 Python 3.7 이상에서 보장됩니다. -
Tip :
pop('key', 기본값)처럼 기본값을 전달하면key가 없을 때KeyError대신 기본값을 반환합니다.
a = {"one": "하나", "two": "둘"}
print(a.pop("one"))
print(a.pop("four", "없음"))
print(a.popitem())
print(a)
try:
a.popitem()
except KeyError as e:
print(e)- 결과
- 하나
없음
(‘two’, ‘둘’)
{}
‘popitem(): dictionary is empty’
a.pop("one")은 "one" 항목을 지우면서 그 value "하나"를 반환합니다. a.pop("four", "없음")은 "four"가 없으므로 아무것도 지우지 않고 기본값 "없음"을 반환합니다.
a.popitem()은 남은 항목 중 마지막 항목을 ('two', '둘') 튜플로 꺼냅니다. 이제 사전이 비었으므로 다시 popitem()을 호출하면 KeyError가 발생합니다.
a = {"a": 1, "b": 2, "c": 3}
try:
for k in a:
if a[k] % 2 == 1:
del a[k]
except RuntimeError as e:
print(e)
a = {"a": 1, "b": 2, "c": 3}
for k in list(a):
if a[k] % 2 == 1:
del a[k]
print(a)- 결과
- dictionary changed size during iteration
{‘b’: 2}
사전을 반복하는 도중에 항목을 추가하거나 삭제하면 RuntimeError가 발생합니다. 반복하는 동안 사전의 크기가 바뀌면 다음에 어떤 key를 꺼내야 할지 알 수 없기 때문입니다.
이때는 list(a)로 key의 목록을 미리 복사한 뒤, 복사본을 반복하면서 원본 사전에서 삭제합니다. 값이 홀수인 "a"와 "c"가 지워지고 {'b': 2}만 남습니다.
조사
사전에 어떤 key와 value가 들어 있는지 확인하는 방법입니다. 사전 전체를 반복하거나, 특정 key가 있는지 검사할 때 사용합니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
print(a.items())
print(a.keys())
print(a.values())
print("one" in a)
print("four" not in a)- 결과
- dict_items([(‘one’, ‘하나’), (‘two’, ‘둘’), (‘three’, ‘셋’)])
dict_keys([‘one’, ‘two’, ‘three’])
dict_values([‘하나’, ‘둘’, ‘셋’])
True
True
사전.items()를 이용하여 (key, value) 형태의 튜플(Tuple)들을 담은 dict_items 객체를 반환합니다.
사전.keys()을 이용하여 모든 key를 담은 dict_keys 객체를 반환합니다.
사전.values()을 이용하여 모든 value를 담은 dict_values 객체를 반환합니다.
key in 사전을 이용하여 사전에 key 값이 있는지 확인합니다.
key not in 사전을 이용하여 사전에 key 값이 없는지 확인합니다.
-
Tip :
items(),keys(),values()가 반환하는 객체는 목록이 아닌뷰(View)객체이며, 사전이 변경되면 그 내용도 함께 바뀝니다. 목록이 필요하다면list(사전.keys())처럼 변환합니다. -
Tip :
in연산자는value가 아닌 key를 기준으로 확인합니다.
a = {"one": 1}
keys = a.keys()
print(keys)
a["two"] = 2
print(keys)
print(1 in a, 1 in a.values())- 결과
- dict_keys([‘one’])
dict_keys([‘one’, ‘two’])
False True
keys는 사전의 key를 복사해 둔 것이 아니라 사전을 들여다보는 창(View)입니다. 그래서 keys를 만든 뒤 "two"를 추가해도, 다시 출력하면 'two'가 포함되어 있습니다.
1 in a는 key 중에 1이 있는지 확인하므로 False이고, value에서 찾으려면 1 in a.values()처럼 명시해야 합니다.
두 검사는 속도도 다릅니다. key in 사전은 해시값으로 위치를 바로 계산하므로 사전의 크기와 상관없이 빠르지만, 값 in 사전.values()는 목록처럼 모든 value를 처음부터 하나씩 비교합니다. value로 자주 검색해야 한다면 value를 key로 하는 사전을 따로 만드는 것이 좋습니다.
a = {"one": "하나", "two": "둘", "three": "셋"}
for key, value in a.items():
print(key, value)- 결과
- one 하나
two 둘
three 셋
items()는 (key, value) 튜플을 차례대로 돌려주므로, 11강에서 배운 언패킹으로 key와 value를 동시에 받을 수 있습니다. 사전 전체를 반복할 때 가장 많이 사용하는 형태입니다.
for key in a:처럼 사전을 그대로 반복하면 key만 나옵니다.
순서
Python 3.7 이전에는 사전의 출력 순서가 보장되지 않았지만, 현재는 넣은 순서가 유지됩니다. 순서가 유지된다는 것이 정확히 무엇을 의미하는지 확인합니다.
a = {"c": 3, "a": 1}
a["b"] = 2
print(a)
print(list(a))
print({"a": 1, "b": 2} == {"b": 2, "a": 1})- 결과
- {‘c’: 3, ‘a’: 1, ‘b’: 2}
[‘c’, ‘a’, ‘b’]
True
Python 3.7 이상에서 사전은 key를 추가한 순서대로 저장하고 출력합니다. key의 크기나 사전 순서로 정렬하지 않습니다.
사전을 반복하거나 list(사전)으로 변환할 때도 삽입 순서대로 key가 나열됩니다.
- Tip : 순서를 유지하더라도 사전끼리 비교(
==)할 때는 순서와 관계없이key와value가 같으면True가 됩니다.
key를 정렬된 순서로 보고 싶다면 sorted(사전)을 사용합니다. 사전 자체는 그대로 두고 정렬된 key의 목록을 반환합니다.
a = {"x": [1, 2]}
b = a.copy()
b["y"] = 3
b["x"].append(3)
print(a)
print(b)- 결과
- {‘x’: [1, 2, 3]}
{‘x’: [1, 2, 3], ‘y’: 3}
사전.copy()는 새 사전을 만드는 얕은 복사입니다. 그래서 b에 "y"를 추가해도 a에는 영향이 없습니다.
하지만 value로 들어 있는 목록은 복사되지 않고 같은 목록을 공유하므로, b["x"].append(3)의 결과가 a에서도 보입니다. 안쪽까지 독립된 복사본이 필요하면 8강의 copy.deepcopy()를 사용합니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
{k: v}, dict(...) |
사전 생성 | 같은 key는 마지막 값만 남음 |
dict.fromkeys(keys, v) |
같은 초깃값으로 생성 | 모든 key가 v 객체 하나를 공유 |
사전[key] |
조회 | value, 없으면 KeyError |
사전.get(key, d) |
안전한 조회 | value 또는 d(기본 None), 사전 변경 없음 |
사전[key] = v |
추가 또는 수정 | 원본 변경 |
사전.setdefault(key, v) |
없을 때만 추가 | 현재 value |
사전.update(x) |
여러 항목 추가·수정 | None, 원본 변경 |
사전1 | 사전2 |
병합 | 새 사전, Python 3.9 이상 |
사전1 |= 사전2 |
병합 | 원본 변경, Python 3.9 이상 |
del 사전[key] |
삭제 | 문장, 없으면 KeyError |
사전.pop(key, d) |
꺼내며 삭제 | value 또는 d |
사전.popitem() |
마지막 항목 꺼내기 | (key, value), 비었으면 KeyError |
사전.clear() |
전체 삭제 | None |
keys(), values(), items() |
뷰 객체 | 사전 변경이 바로 반영 |
key in 사전 |
key 존재 여부 | 논리값, 해시로 바로 찾음 |
사전.copy() |
얕은 복사 | 새 사전 |
사전은 변경 가능하고, 삽입 순서를 유지하며, key는 중복될 수 없는 해시 기반 자료형입니다. key의 해시값으로 저장 위치를 계산하기 때문에 크기와 상관없이 빠르게 조회할 수 있고, 그 대가로 key는 해시 가능한 객체여야 합니다.
댓글 남기기