Skip to content
This page has been auto-translated and may contain errors.View in English

딕셔너리

리스트는 위치로 항목을 찾을 수 있게 해줍니다. 하지만 종종 이름으로 무언가를 찾고 싶을 때가 있습니다. "3번 항목을 줘"가 아니라 "민준의 점수를 줘"처럼요. 딕셔너리는 데이터를 키-값 쌍으로 저장합니다: 위치가 아닌 키로 값을 찾습니다.

리스트의 위치 인덱스가 의미가 없을 때, 딕셔너리가 적절한 자료구조입니다. 딕셔너리는 임의의 키를 값에 매핑하여 O(1) 시간에 이름으로 조회할 수 있게 해줍니다. 리더보드, JSON 응답, 설정 파일 등은 모두 자연스럽게 키-값 매핑으로 표현됩니다.

dict는 해시 테이블 기반의 키-값 저장소로, 평균 O(1) 조회, 삽입, 삭제 성능을 가집니다. 키는 해시 가능해야 하지만, 값은 어떤 객체든 될 수 있습니다. Python 3.7부터 딕셔너리는 삽입 순서를 보존합니다. dict는 Python의 네임스페이스, 객체의 __dict__ 속성, 키워드 인자의 기반이 됩니다.

딕셔너리 생성하기

각 키와 값 사이에 콜론을 두고, 쌍 사이에 콤마를 두는 중괄호 구문을 사용합니다. 키는 거의 항상 문자열입니다. 값은 숫자, 문자열, 다른 리스트, 심지어 다른 딕셔너리 등 무엇이든 될 수 있습니다.

딕셔너리 리터럴은 key: value 구문과 함께 중괄호를 사용합니다. 키는 문자열, 정수, 튜플 등 불변(해시 가능) 타입이라면 무엇이든 될 수 있습니다. 값은 어떤 Python 객체든 가능합니다. 딕셔너리는 삽입 순서를 보존하므로, 순회할 때 추가된 순서대로 항목을 얻습니다.

딕셔너리 리터럴은 왼쪽에서 오른쪽으로 평가됩니다. 키는 해시 가능해야 합니다: str, int, tuple은 가능하지만 listdict는 불가능합니다. 값은 제한이 없습니다. 삽입 순서는 Python 3.7부터 보장됩니다 (3.6부터 컴팩트 해시 테이블로 구현됨). 리터럴 내 중복 키는 마지막 값을 조용히 사용합니다.

python
player = {
    "name":  "민준",
    "score": 87,
    "level": 5,
    "alive": True,
}

값 접근하기

키를 대괄호와 함께 사용하여 값을 얻습니다. 키가 존재하지 않으면 Python은 KeyError를 발생시킵니다. 키가 있는지 확실하지 않을 때는 .get()을 사용하세요: 충돌 대신 None이나 지정한 기본값을 반환합니다.

대괄호 접근은 누락된 키에 대해 KeyError를 발생시킵니다. .get(key)는 미스에 대해 None을 반환합니다. .get(key, default)는 대신 기본값을 반환합니다. 키 존재 여부가 불확실할 때는 항상 .get()을 사용하세요; 접근을 try/except로 감싸는 것보다 안전하고 가독성이 좋습니다.

d[key]__getitem__을 호출하며, 키를 해시하고 테이블을 탐색합니다: 평균 O(1). 미스 시 KeyError를 발생시킵니다. .get(key, default=None)은 동일한 탐색을 수행하지만 미스 시 예외를 발생시키는 대신 기본값을 반환합니다. key in d 검사(__contains__를 호출)는 O(1)이며, 접근 전 가드를 위한 관용적인 방법입니다.

python
player = {"name": "민준", "score": 87}

player["name"]    # "민준"
player["score"]   # 87
player["lives"]   # KeyError (key doesn't exist)
python
player.get("score")          # 87
player.get("lives")          # None (no error, returns None by default)
player.get("lives", 3)       # 3   (use this default if key is absent)

키가 누락될 가능성이 있을 때마다 .get()이 더 안전합니다:

python
count = inventory.get("arrows", 0)   # 0 if "arrows" isn't in the dict

추가 및 업데이트

대괄호로 키에 할당합니다. 키가 이미 존재하면 값이 교체됩니다. 아직 존재하지 않으면 새 항목이 생성됩니다. 전체 다른 딕셔너리를 한 번에 병합하려면 .update()를 사용하세요.

키에 대한 할당은 __setitem__을 호출합니다: 평균 O(1)이며, 생성하거나 교체합니다. .update()는 다른 딕셔너리나 키-값 쌍의 이터러블을 받아 각 항목에 대해 __setitem__을 호출하여 기존 키를 덮어씁니다.

d[key] = value__setitem__을 호출하며, 키를 해시하고 테이블에 삽입하거나 덮어씁니다: 평균 O(1). .update(other)는 반복된 __setitem__ 호출과 동등합니다. | 연산자 (Python 3.9+)는 변경 없이 딕셔너리를 병합하고 새 딕셔너리를 반환합니다; |=는 제자리에서 변경합니다.

python
player = {"name": "민준", "score": 87}

player["score"] = 92        # update existing
player["level"] = 5         # add new key
python
extras = {"level": 5, "alive": True}
player.update(extras)   # adds/overwrites with keys from extras

항목 제거하기

항목을 제거하는 네 가지 방법이 있습니다. .pop()은 키를 제거하고 값을 반환합니다. 기본값과 함께 사용한 .pop()은 키가 없을 때도 안전합니다. del은 반환값 없이 키를 제거합니다. .clear()는 전체 딕셔너리를 비웁니다.

.pop(key)는 미스 시 KeyError를 발생시킵니다. .pop(key, default)는 대신 기본값을 반환하여 안전한 제거 메서드가 됩니다. del d[key]__delitem__을 호출하고 미스 시 KeyError를 발생시킵니다. .clear()는 모든 항목을 제거하지만 딕셔너리 객체 자체는 유지합니다.

.pop(key, default)는 단일 해시 탐색입니다: 평균 O(1). del d[key]__delitem__을 호출하며 같은 탐색을 하고 미스 시 예외를 발생시킵니다. 제거 후 해시 테이블은 메모리를 해제하기 위해 축소될 수 있습니다. .clear()는 테이블 크기를 재설정합니다. 같은 루프에서 딕셔너리를 순회하면서 변경하면 RuntimeError가 발생합니다; 먼저 제거할 키 목록을 빌드하세요.

python
player = {"name": "민준", "score": 87, "level": 5}

player.pop("level")            # removes "level" and returns 5
player.pop("lives", None)      # safe pop, returns None if key absent
del player["score"]            # removes "score", no return value
player.clear()                 # removes everything

기본값을 사용한 .pop()은 존재하지 않을 수 있는 키를 제거하는 가장 안전한 방법입니다.

순회하기

세 가지 뷰를 통해 딕셔너리의 여러 부분을 순회할 수 있습니다. 딕셔너리 자체를 순회하면 키를 얻습니다. .values()는 값을 줍니다. .items()는 둘 다 한 번에 주며 가장 많이 사용하게 될 것입니다: 각 쌍을 두 이름으로 언패킹하여 깔끔하고 읽기 쉬운 루프를 만드세요.

.keys(), .values(), .items()는 리스트가 아닌 뷰 객체를 반환합니다. 뷰는 딕셔너리의 현재 상태를 동적으로 반영합니다: 딕셔너리를 수정하면 뷰가 즉시 업데이트됩니다. 대부분의 루프에서는 .items()가 가장 유용한데, 튜플 언패킹 for k, v in d.items()이 명확하게 읽히기 때문입니다.

.keys(), .values(), .items()는 각각 dict_keys, dict_values, dict_items 뷰 객체를 반환합니다. 뷰는 지연(lazy) 평가됩니다: 데이터를 복사하지 않으며 기본 딕셔너리가 변경될 때 업데이트됩니다. 키는 고유하고 해시 가능하므로 dict_keys는 집합 연산(&, |, -)을 지원합니다. 순회 중 딕셔너리 변경은 RuntimeError를 발생시킵니다; 필요하면 list(d.items())로 스냅샷하세요.

python
player = {"name": "민준", "score": 87, "level": 5}

for key in player:               # iterate keys (most common)
    print(key)

for key in player.keys():        # same, explicit keys view
    print(key)

for value in player.values():    # just the values
    print(value)

for key, value in player.items():   # both, most useful
    print(f"{key}: {value}")

.items()가 가장 많이 사용될 것입니다. 각 쌍을 두 이름으로 언패킹하면 루프가 읽기 쉬워집니다.

멤버십 검사하기

in은 딕셔너리에 키가 존재하는지 검사합니다. 값이 아닌 키만 검사합니다. 무언가가 없는지 확인하려면 not in을 사용하세요.

innot in__contains__를 호출하며, 딕셔너리에서는 O(1)입니다. 키만 검사합니다. 값을 검사하려면 in d.values()를 사용하지만, 값은 인덱싱되지 않으므로 O(n)입니다.

key in ddict.__contains__를 호출하여 키를 해시하고 테이블을 탐색합니다: 평균 O(1). value in d.values()는 값 뷰를 순회합니다: O(n). 이 비대칭성은 값을 스캔하는 것보다 조회를 위해 딕셔너리 키를 선호하는 핵심 이유입니다.

python
player = {"name": "민준", "score": 87}

"name"  in player      # True
"lives" in player      # False
"lives" not in player  # True

in은 키만 검사합니다. 값을 검사하려면 in player.values()를 사용하지만, 거의 필요하지 않습니다.

중첩 딕셔너리

값 자체가 딕셔너리일 수 있습니다. 이것은 여러 레벨을 가진 구조화된 데이터를 표현하는 방식입니다: 통계 섹션이 있는 플레이어, 하위 섹션이 있는 설정 파일 등. 두 쌍의 대괄호로 중첩된 값에 접근합니다: 첫 번째는 외부 키를 선택하고, 두 번째는 내부 키를 선택합니다.

중첩 딕셔너리는 값 자체가 딕셔너리인 딕셔너리입니다. 연쇄 첨자로 접근하세요. 외부 딕셔너리는 같은 객체에 대한 참조를 가지고 있기 때문에 내부 딕셔너리를 변경하면 외부 딕셔너리에 영향을 미칩니다. 가능하면 중첩을 얕게 유지하세요: 깊은 중첩은 빠르게 읽고 탐색하기 어려워집니다.

중첩 딕셔너리는 객체 참조를 저장하지 사본을 저장하지 않습니다. 외부 딕셔너리의 얕은 복사(d.copy())는 내부 딕셔너리를 복사하지 않으므로, 내부 딕셔너리에 대한 변경은 원본과 사본 모두에서 보입니다. 깊게 중첩된 구조의 경우 copy.deepcopy()가 완전히 독립적인 사본을 만듭니다. 연쇄 __getitem__ 호출은 각각 O(1)이므로 접근 깊이는 점근적 비용이 없습니다.

python
users = {
    "민준": {"score": 87, "level": 5},
    "서연": {"score": 74, "level": 3},
}

users["민준"]["score"]   # 87
users["서연"]["level"]     # 3

연쇄 대괄호로 접근합니다. 깊게 중첩된 구조의 경우 다루기 어려울 수 있으므로, 가능한 한 중첩을 얕게 유지하세요.

setdefault

.setdefault()는 키가 존재하면 읽고, 존재하지 않으면 기본값으로 설정한 다음 값을 반환합니다. 키가 존재해야 하지만 이미 있다면 덮어쓰지 않으려는 경우에 유용합니다.

.setdefault(key, default)는 원자적 읽기 또는 생성입니다: 키가 존재하면 아무 것도 변경하지 않고 현재 값을 반환하고; 존재하지 않으면 기본값을 삽입하고 반환합니다. 일반적인 사용 사례는 별도의 존재 확인 없이 그룹화된 구조를 빌드하는 것입니다.

.setdefault(key, default)는 단일 해시 탐색입니다: 평균 O(1). 키가 없으면 default가 삽입되고 반환됩니다. 존재하면 기존 값이 반환되고 default는 무시됩니다 (검사 후 평가되지 않음). 일반적인 "리스트에 항목 그룹화" 패턴의 경우, 추가 전에 key in d를 확인하는 것의 표준 대안입니다.

python
inventory = {}

inventory.setdefault("arrows", 0)    # sets "arrows": 0, returns 0
inventory.setdefault("arrows", 10)   # "arrows" already exists, no change, returns 0

키 존재 여부를 먼저 확인하지 않고 그룹화된 구조를 빌드하는 데 유용합니다:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)

collections.defaultdict 및 Counter

표준 라이브러리에는 일반적인 패턴을 자동으로 처리하는 두 개의 딕셔너리 하위 클래스가 있습니다. defaultdict는 누락된 키에 대한 기본값을 생성하므로 KeyError가 발생하지 않습니다. Counter는 시퀀스에서 각 항목이 얼마나 자주 나타나는지 세어 결과를 딕셔너리로 제공합니다.

defaultdict는 새 키에 대한 기본값을 생성하는 호출 가능 객체를 받아 .setdefault()의 필요성을 제거합니다. Counter.most_common() 메서드를 가진 빈도 카운팅을 위한 특수한 딕셔너리입니다. 둘 다 딕셔너리 하위 클래스이므로 모든 표준 딕셔너리 연산이 작동합니다.

defaultdict.__missing__은 팩토리를 호출하고 결과를 저장하므로 일반적인 경우에 스레드 안전합니다. Counterdict를 하위 클래스로 하며 .most_common(n) (heapq를 통해 O(n log n)), .subtract(), 그리고 카운트 결합을 위한 산술 연산자를 추가합니다. 둘 다 collections에 있으며; 임포트는 Modules 챕터에서 다룹니다.

collections import

defaultdictCounter는 표준 라이브러리에서 임포트해야 합니다. 임포트는 Modules 챕터에서 다룹니다.

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # no KeyError if team is new
python
from collections import Counter

words  = ["cat", "dog", "cat", "bird", "cat", "dog"]
counts = Counter(words)
# Counter({'cat': 3, 'dog': 2, 'bird': 1})

counts.most_common(2)   # [('cat', 3), ('dog', 2)]

Counter는 "루프에서 항목 세기" 보일러플레이트를 많이 절약해줍니다.

실전에서

점수 트래커를 빌드하고 모든 항목과 함께 요약을 출력하기:

python
scores = {"민준": 87, "서연": 92, "지호": 74, "하윤": 55}

total   = sum(scores.values())
average = total / len(scores)

print(f"Players:  {len(scores)}")
print(f"Average:  {average:.1f}")
print(f"Highest:  {max(scores.values())}")
print(f"Lowest:   {min(scores.values())}")
print()

for name, score in scores.items():
    print(f"  {name}: {score}")

루프에서 파일별 결과의 딕셔너리를 빌드한 다음 모든 항목에 대해 요약하기:

python
job_results = {}
files       = ["report_jan.csv", "report_feb.csv", "report_mar.csv"]

for filename in files:
    size = len(filename) * 100   # placeholder for real file size
    if size < 2000:
        status = "ok"
    else:
        status = "large"
    job_results[filename] = {"size": size, "status": status}

ok_count    = 0
large_count = 0

for result in job_results.values():
    if result["status"] == "ok":
        ok_count += 1
    else:
        large_count += 1

print(f"Processed {len(job_results)} file(s): {ok_count} ok, {large_count} large")

필수 필드를 순회하여 중첩 요청 딕셔너리를 검증하고, 그 다음 피처 중요도 딕셔너리를 제자리에서 정규화하기:

python
request = {
    "method":  "POST",
    "path":    "/users",
    "headers": {"Content-Type": "application/json"},
    "body":    {"username": "민준", "email": "[email protected]"},
}

body   = request["body"]
errors = []

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"Missing required field: {field}")

if "email" in body and "@" not in body["email"]:
    errors.append("Invalid email format")

print(f"Method: {request['method']} {request['path']}")
if errors:
    print(f"Errors: {errors}")
else:
    print("Validation passed")

# Normalise feature importance values to sum to 1
feature_importance = {"age": 0.34, "income": 0.28, "region": 0.15, "purchases": 0.23}
total = sum(feature_importance.values())

for key in feature_importance:
    feature_importance[key] = round(feature_importance[key] / total, 3)

print(f"Normalised: {feature_importance}")