파이썬 딕셔너리(dict): 만들기, 메서드, 순회, 정렬, frozendict
파이썬 딕셔너리(dict)는 키와 값의 쌍을 저장하는 변경 가능한 자료형으로, {"커피": 250}처럼 중괄호로 만들거나 dict() 함수로 만듭니다. 키는 중복될 수 없고 해시 가능해야 하며, 값은 키로 거의 즉시 찾습니다. Python 3.7부터 삽입 순서를 유지하고, Python 3.15에는 변경할 수 없는 내장 타입 frozendict가 추가되었습니다.
파이썬 딕셔너리(dict)는 키와 값의 쌍을 저장하는 자료형입니다. 항목이 아무리 많아도 키로 값을 찾는 것은 거의 즉시 끝납니다. 카테고리별 지출을 집계하거나, 설정값을 저장하거나, 사용자 ID로 사용자를 찾을 때 딕셔너리를 씁니다. 이 글에서는 딕셔너리 만들기, 메서드, 순회, 정렬, 병합, 그리고 Python 3.15의 새 타입 frozendict까지 정리합니다.
딕셔너리란
딕셔너리에는 키가 중복되지 않습니다. 이미 있는 키에 값을 쓰면 이전 값이 교체됩니다. 내부적으로는 해시 테이블이므로, 키로 검색, 추가, 삭제하는 작업은 전체를 훑지 않고 평균적으로 일정한 시간에 끝납니다.
키는 해시 가능한 객체여야 합니다. 문자열, 숫자, 변경할 수 없는 값으로 이루어진 튜플이 해당합니다. 리스트를 키로 쓰면 TypeError: unhashable type: 'list'가 납니다. 값은 무엇이든 될 수 있으며, 다른 딕셔너리나 리스트도 가능합니다.
Python 3.7부터 딕셔너리는 항목을 넣은 순서를 보장합니다. 순회하면 키가 추가된 순서대로 나옵니다.
딕셔너리 만들기
딕셔너리를 만드는 방법은 여러 가지이며, 실제 코드에서 모두 볼 수 있습니다.
spent = {"식비": 1200, "택시": 450} # 리터럴
empty = {} # 빈 딕셔너리
also_empty = dict() # dict 함수로 만들기
user = dict(name="안나", age=30) # 키워드 인자
pairs = dict([("a", 1), ("b", 2)]) # 쌍의 리스트에서
keys = dict.fromkeys(["x", "y"], 0) # {'x': 0, 'y': 0}
빈 중괄호 {}는 빈 딕셔너리를 만듭니다. 빈 집합은 set()으로만 만듭니다.
두 리스트로 딕셔너리를 만들 때는 zip이 편리합니다.
names = ["식비", "택시", "영화"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent) # {'식비': 1200, '택시': 450, '영화': 600}
키로 값 읽기: d[k]와 get
값은 대괄호로 읽습니다. 키가 없으면 파이썬은 KeyError를 냅니다. 오류 없이 읽으려면 get 메서드를 씁니다. 키가 없을 때 None이나 지정한 기본값을 돌려줍니다.
spent = {"식비": 1200}
print(spent["식비"]) # 1200
print(spent.get("영화")) # None
print(spent.get("영화", 0)) # 0
spent["영화"] # KeyError: '영화'
키가 있는지는 in 연산자로 확인합니다: "식비" in spent는 True를 반환합니다. in은 키만 검사하며 값은 보지 않습니다. 값을 확인하려면 1200 in spent.values()를 쓰세요.
추가, 변경, 삭제
항목을 추가하거나 값을 바꾸는 것은 대입 한 줄입니다. 집계를 할 때는 get과 함께 쓰면 편합니다.
spent = {}
for category, amount in [("식비", 250), ("택시", 300), ("식비", 400)]:
spent[category] = spent.get(category, 0) + amount
print(spent) # {'식비': 650, '택시': 300}
키를 지우려면 del d[k]를 씁니다. 키가 없으면 KeyError가 납니다. d.pop(k, default)는 키를 지우면서 값을 돌려주고, 키가 없으면 기본값을 돌려줍니다. 전부 비우려면 d.clear(), 항목 수는 len(d)입니다.
딕셔너리 메서드 정리
| 메서드 | 하는 일 |
|---|---|
get(k, default) | 키의 값, 없으면 default를 오류 없이 반환 |
setdefault(k, default) | 값을 반환하고, 키가 없으면 먼저 default를 저장 |
update(other) | 다른 딕셔너리의 쌍을 추가하고 덮어쓰기 |
pop(k, default) | 키를 지우고 값을 반환 |
popitem() | 마지막으로 추가된 쌍을 지우고 반환 |
keys(), values(), items() | 키, 값, 쌍의 뷰 |
copy() | 얕은 복사 |
clear() | 모두 비우기 |
fromkeys(keys, value) | 같은 값을 가진 새 딕셔너리 만들기 |
setdefault: 리스트 값으로 그룹화하기
값이 리스트이고 항목을 묶어야 할 때 setdefault가 도움이 됩니다.
groups = {}
for item, cat in [("커피", "식비"), ("지하철", "교통"), ("피자", "식비")]:
groups.setdefault(cat, []).append(item)
print(groups) # {'식비': ['커피', '피자'], '교통': ['지하철']}
update: 딕셔너리 갱신
update는 딕셔너리를 제자리에서 바꿉니다. 새 키는 추가되고, 같은 키는 덮어씁니다.
cfg = {"debug": False, "lang": "ko"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg) # {'debug': True, 'lang': 'ko', 'theme': 'dark'}
defaultdict: 기본값이 있는 딕셔너리
collections.defaultdict는 dict의 하위 클래스입니다. 없는 키에 접근하면 지정한 팩토리 함수로 값을 자동으로 만듭니다. 집계와 그룹화 코드가 짧아집니다.
from collections import defaultdict
spent = defaultdict(int)
for cat, amount in [("식비", 250), ("택시", 300), ("식비", 400)]:
spent[cat] += amount
print(dict(spent)) # {'식비': 650, '택시': 300}
groups = defaultdict(list)
groups["식비"].append("커피")
단순히 개수만 세려면 collections.Counter도 있습니다. Counter(["a", "b", "a"])는 Counter({'a': 2, 'b': 1})를 만듭니다.
for 반복문으로 딕셔너리 순회하기
딕셔너리를 for로 순회하면 기본적으로 키가 나옵니다. 키와 값을 함께 얻으려면 items()를 쓰는 것이 가장 흔합니다.
spent = {"식비": 650, "택시": 300}
for cat in spent: # 키
print(cat)
for amount in spent.values(): # 값
print(amount)
for cat, amount in spent.items(): # 쌍
print(f"{cat}: {amount}원")
순회하는 도중에 딕셔너리 크기를 바꾸면 RuntimeError: dictionary changed size during iteration이 납니다. 일부 키를 지워야 한다면 복사본을 순회하세요: for k in list(d):.
딕셔너리 출력하기
print(d)는 딕셔너리를 한 줄로 출력합니다. 크거나 중첩된 딕셔너리는 pprint 모듈이나 json.dumps(d, ensure_ascii=False, indent=2)가 더 읽기 좋습니다. 들여쓰기가 적용되고, 한글도 이스케이프되지 않고 나옵니다.
딕셔너리 정렬
딕셔너리 자체를 제자리에서 정렬할 수는 없습니다. 딕셔너리에는 sort 메서드가 없습니다. 정렬된 쌍을 sorted로 얻은 뒤 새 딕셔너리를 만듭니다.
spent = {"택시": 300, "식비": 650, "영화": 120}
by_key = dict(sorted(spent.items()))
print(by_key) # {'식비': 650, '영화': 120, '택시': 300}
by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value) # {'식비': 650, '택시': 300, '영화': 120}
상위 N개 카테고리만 필요하다면 다시 딕셔너리로 바꾸지 마세요. sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2]는 쌍의 리스트를 반환합니다.
딕셔너리의 리스트를 어떤 필드로 정렬하려면 이렇게 씁니다: sorted(users, key=lambda u: u["age"]).
딕셔너리 병합과 더하기
Python 3.9부터는 | 연산자로 딕셔너리를 합칠 수 있고, |=는 제자리에서 갱신합니다. 키가 겹치면 오른쪽 피연산자가 이깁니다.
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b) # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b}) # 언패킹으로도 같은 결과
a |= b # a.update(b)와 같습니다
딕셔너리끼리 a + b는 동작하지 않으며 TypeError가 납니다. 같은 키의 값을 더하고 싶다면 Counter를 쓰세요: Counter(a) + Counter(b).
딕셔너리 언패킹
별표 두 개(**)는 딕셔너리를 함수의 키워드 인자로 풀어 넣거나, 다른 딕셔너리에 펼쳐 넣습니다.
def greet(name, city):
return f"{name}님은 {city} 출신입니다"
data = {"name": "안나", "city": "서울"}
print(greet(**data)) # 안나님은 서울 출신입니다
딕셔너리 컴프리헨션
딕셔너리 컴프리헨션은 어떤 시퀀스로부터 한 줄에 딕셔너리를 만듭니다.
squares = {n: n * n for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
big = {k: v for k, v in spent.items() if v > 200}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted) # {1: 'a', 2: 'b'}
마지막 예는 키와 값을 바꿔 딕셔너리를 뒤집는 방법입니다. 값이 고유하고 해시 가능할 때만 동작합니다. 그렇지 않으면 일부 쌍이 사라집니다.
중첩 딕셔너리
값으로 또 다른 딕셔너리를 넣을 수 있습니다. API 응답 같은 JSON 구조나, 사용자 ID별 사용자 정보를 담는 '딕셔너리의 딕셔너리'가 이런 형태입니다.
users = {
101: {"name": "안나", "skills": ["python", "sql"]},
102: {"name": "올레", "skills": ["go"]},
}
print(users[101]["skills"][0]) # python
print(users.get(103, {}).get("name")) # None, KeyError 없음
딕셔너리 복사: 복사본과 같은 객체의 다른 이름
b = a는 딕셔너리를 복사하지 않고, 같은 객체에 이름을 하나 더 붙입니다. b를 바꾸면 a도 바뀝니다. 함수에 딕셔너리를 넘길 때도 같은 일이 생깁니다. 함수는 공유된 객체의 참조를 받으므로 몰래 망가뜨릴 수 있습니다.
import copy
a = {"limits": [0, 500]}
b = a.copy() # 얕은 복사
b["limits"].append(1000) # 안쪽 리스트는 공유됩니다!
print(a) # {'limits': [0, 500, 1000]}
c = copy.deepcopy(a) # 완전히 독립된 복사본
딕셔너리 비교
==는 내용을 비교합니다. 모든 쌍이 같으면 같다고 봅니다. 키의 순서는 고려하지 않습니다. {"a": 1, "b": 2} == {"b": 2, "a": 1}은 True입니다. 공통 키를 찾으려면 뷰에 집합 연산을 쓰세요: a.keys() & b.keys().
타입 힌트: 딕셔너리 타입 표기
딕셔너리 타입은 dict[str, int]처럼 표기합니다. 필드마다 타입이 다른 고정된 구조라면 typing 모듈의 TypedDict가 적합합니다.
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "안나", "age": 30}
타입 힌트는 실행할 때 검사되지 않습니다. IDE와 mypy 같은 분석기가 읽고 확인합니다.
변경할 수 없는 딕셔너리: Python 3.15의 frozendict
Python 3.15에는 내장 타입 frozendict가 추가되었습니다(PEP 814). 별도로 import할 필요가 없고, 수정을 시도하면 바로 TypeError가 납니다. 이전에는 types 모듈의 래퍼나 외부 패키지로 불변 딕셔너리를 흉내 내야 했습니다. 어떤 함수도 몰래 바꾸면 안 되는 공용 설정값에 딱 맞는 도구입니다.
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment
strict = config | {"max_amount": 300} # 새 frozendict, 원본은 그대로
cache = {config: "5월 보고서"} # 해시 가능하므로 키로 사용 가능
강의 자료 기준으로 알아 둘 점은 다음과 같습니다.
frozendict는dict가 아니라object를 상속합니다. 그래서isinstance(cfg, dict)는False를 반환합니다. '모든 딕셔너리'를 검사하려면collections.abc.Mapping이나isinstance(x, (dict, frozendict))를 쓰세요.- 해시 가능 여부는 키와 값이 해시 가능한지에 달려 있습니다.
frozendict(a=[1])은 만들어지지만hash()에서 실패합니다. 불변성은 튜플처럼 얕습니다. - 순서는 보존되지만
==는 순서를 고려하지 않습니다. json.dumps,pickle,copy는 frozendict를 지원합니다.
자주 하는 실수
- 읽을 때 KeyError —
get을 쓰거나in으로 먼저 확인하세요. - 리스트를 키로 사용 — 튜플로 바꾸세요:
d[tuple(lst)]. - 순회 중 딕셔너리 변경 —
list(d)를 순회하세요. - 대입이 복사를 만든다고 기대 —
copy()나deepcopy를 쓰세요. - 빈 집합을
{}로 만들기 — 그것은 빈 딕셔너리입니다.
정리
파이썬 dict는 가장 중요한 자료구조입니다. 키로 빠르게 찾고, 삽입 순서를 기억하며, get, setdefault, update 같은 메서드와 items() 순회, sorted 정렬, | 병합을 제공합니다. 복사와 해시 가능한 키를 주의하고, Python 3.15에서 설정값에는 frozendict를 쓰세요. 딕셔너리, 리스트, 튜플, 집합은 '파이썬 3.15: 초보부터 전문가까지' 과정에서 지출 관리 예제로 실습합니다.
자주 묻는 질문
딕셔너리 값에 서로 다른 타입을 넣어도 되나요?
네. 값은 숫자, 문자열, 리스트, 다른 딕셔너리 등 무엇이든 가능하고 한 딕셔너리 안에서 섞어 써도 됩니다. 제한은 키에만 있으며, 키는 해시 가능해야 합니다.
딕셔너리의 항목 수는 어떻게 알 수 있나요?
len(d)가 키-값 쌍의 개수를 반환합니다. 빈 딕셔너리는 0을 반환하고, if 조건에서는 거짓으로 취급됩니다.
dict와 defaultdict는 무엇이 다른가요?
defaultdict는 dict의 하위 클래스로, 없는 키에 접근해도 KeyError 대신 int나 list 같은 팩토리로 값을 만듭니다. 나머지 동작은 일반 딕셔너리와 같습니다.
값으로 키를 찾으려면 어떻게 하나요?
바로 가져오는 메서드는 없으므로 순회해야 합니다: next((k for k, v in d.items() if v == value), None). 자주 필요하다면 {v: k for k, v in d.items()}로 역방향 딕셔너리를 만들어 두세요.
딕셔너리를 파일로 저장하려면?
보통 json 모듈을 씁니다. 쓰기용으로 연 파일에 json.dump(d, f, ensure_ascii=False, indent=2)를 쓰고, 읽을 때는 json.load(f)를 씁니다. JSON에서는 키가 문자열이 됩니다.