Python & AI Tutorials Logo
Python 프로그래밍

16. 딕셔너리: 키로 값 매핑하기

이전 장들에서는 리스트와 튜플을 배웠습니다. 이들은 특정한 순서로 항목을 저장하고, 위치로 접근할 수 있는 컬렉션입니다. 하지만 숫자보다 더 의미 있는 무언가로 정보를 찾아보고 싶다면 어떻게 해야 할까요? 학생의 이름으로 성적을 찾거나, 제품의 ID로 가격을 찾거나, 단어 자체로 그 단어의 정의를 찾고 싶다면 어떻게 해야 할까요?

바로 이때 딕셔너리(dictionary) 가 등장합니다. 딕셔너리는 키-값 쌍(key-value pairs) 을 저장하기 위한 Python의 내장 자료구조입니다. 항목을 위치(예: grades[0])로 접근하는 대신, 키(key) (예: grades["Alice"])로 접근합니다. 이 덕분에 딕셔너리는 실제 프로그램에서 데이터를 정리하고 검색하는 데 매우 강력합니다.

딕셔너리를 실제 세상의 사전이나 전화번호부로 생각해 보세요. 단어(키)를 찾아 정의(값)를 얻거나, 이름을 찾아 전화번호를 찾습니다. Python 딕셔너리도 똑같이 동작합니다. 키를 값에 매핑하여 빠른 조회와 유연한 데이터 구성을 가능하게 합니다.

16.1) 딕셔너리 만들기와 값에 접근하기

16.1.1) 딕셔너리란?

딕셔너리(dictionary)키-값 쌍(key-value pairs) 의 컬렉션입니다. 각 키는 어떤 값과 연결되어 있으며, 키를 사용해 값을 가져옵니다. 키는 딕셔너리 안에서 유일해야 합니다. 즉, 같은 키를 가진 항목을 두 개 가질 수 없습니다. 반면 값은 중복될 수 있습니다.

기본 구조는 다음과 같습니다:

  • 키(Keys): 값 조회에 사용하는 고유 식별자(이름, ID, 라벨 등)
  • 값(Values): 각 키에 연결된 데이터(성적, 가격, 설명 등)

딕셔너리의 특징은 다음과 같습니다:

  • 가변(mutable): 생성 후에도 키-값 쌍을 추가, 수정, 삭제할 수 있습니다
  • 순서 없음(unordered)(Python 3.6 이전) 또는 삽입 순서 유지(insertion-ordered)(Python 3.7+): 최신 Python은 항목을 추가한 순서를 유지하지만, 딕셔너리는 위치가 아니라 키로 접근하는 컬렉션이라고 생각해야 합니다
  • 동적(dynamic): 필요에 따라 크기가 늘거나 줄 수 있습니다

16.1.2) 빈 딕셔너리와 간단한 딕셔너리 만들기

딕셔너리를 만드는 가장 간단한 방법은 중괄호 {} 안에 콜론으로 구분된 키-값 쌍을 넣는 것입니다:

python
# 빈 딕셔너리
empty_dict = {}
print(empty_dict)  # Output: {}
print(type(empty_dict))  # Output: <class 'dict'>
 
# 학생 성적 딕셔너리
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# 제품 가격 딕셔너리
prices = {"apple": 0.50, "banana": 0.30, "orange": 0.75}
print(prices)  # Output: {'apple': 0.5, 'banana': 0.3, 'orange': 0.75}

문법을 보면 각 키-값 쌍은 key: value 형태로 작성되고, 쌍들은 쉼표로 구분됩니다. 여기서 키는 문자열("Alice", "apple")이고 값은 숫자이지만, 키와 값은 매우 다양한 타입이 될 수 있습니다.

dict() 생성자를 사용해서도 딕셔너리를 만들 수 있습니다:

python
# 키워드 인자를 사용한 dict()
student = dict(name="Alice", age=20, major="Computer Science")
print(student)  # Output: {'name': 'Alice', 'age': 20, 'major': 'Computer Science'}
 
# 튜플 리스트를 사용한 dict()
colors = dict([("red", "#FF0000"), ("green", "#00FF00"), ("blue", "#0000FF")])
print(colors)  # Output: {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'}

dict() 생성자는 다른 자료구조로부터 딕셔너리를 만들 때, 또는 (따옴표 없이) Python 식별자를 키로 사용하고 싶을 때 유용합니다.

16.1.3) 키로 값에 접근하기

딕셔너리에서 값을 가져오려면 대괄호에 키를 넣어 사용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 개별 값 접근
alice_grade = grades["Alice"]
print(alice_grade)  # Output: 95
 
bob_grade = grades["Bob"]
print(bob_grade)  # Output: 87

이것이 딕셔너리 값에 접근하는 가장 직접적인 방법입니다. 하지만 존재하지 않는 키에 접근하려고 하면 Python은 KeyError를 발생시킵니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# WARNING: KeyError - for demonstration only
# print(grades["David"])  # PROBLEM: KeyError: 'David'

이 에러는 "David"가 딕셔너리에 있는 키가 아니기 때문에 발생합니다. 다음 하위 섹션에서 이를 안전하게 처리하는 방법을 배우겠습니다.

16.1.4) get()으로 안전하게 접근하기

키가 존재하지 않을 수도 있을 때 KeyError를 피하려면 get() 메서드를 사용하세요. 키를 찾지 못하면 None(또는 지정한 기본값)을 반환합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# get()으로 안전하게 접근
alice_grade = grades.get("Alice")
print(alice_grade)  # Output: 95
 
# 키가 없음 - None 반환
david_grade = grades.get("David")
print(david_grade)  # Output: None
 
# 기본값 제공
david_grade = grades.get("David", 0)
print(david_grade)  # Output: 0
 
# 조건 로직에서 get() 사용
if grades.get("Eve") is None:
    print("Eve is not in the grade book")  # Output: Eve is not in the grade book

get() 메서드는 키가 존재하는지 확신할 수 없을 때 대괄호로 직접 접근하는 것보다 안전합니다. get()의 두 번째 인자는 키가 없을 때 반환할 기본값이며, 이를 제공하지 않으면 기본값은 None입니다.

다음은 get()이 유용한 상황을 보여주는 실용적인 예시입니다:

python
# 선택 정보가 있을 수도 있는 학생 데이터베이스
students = {
    "Alice": {"age": 20, "major": "CS"},
    "Bob": {"age": 19},  # Bob은 아직 전공을 선언하지 않았습니다
    "Charlie": {"major": "Math"}  # Charlie의 나이는 기록되지 않았습니다
}
 
# 누락될 수 있는 정보를 안전하게 접근
for name in ["Alice", "Bob", "Charlie"]:
    student = students[name]
    age = student.get("age", "Unknown")
    major = student.get("major", "Undeclared")
    print(f"{name}: Age {age}, Major {major}")
 
# Output:
# Alice: Age 20, Major CS
# Bob: Age 19, Major Undeclared
# Charlie: Age Unknown, Major Math

16.1.5) 유효한 키 타입

딕셔너리의 키는 해시 가능(hashable) 해야 합니다. 이는 키의 값이 바뀔 수 없다는 뜻의 기술 용어입니다. 실제로는 다음을 의미합니다:

유효한 키 타입(불변, immutable):

  • 문자열: "name", "id_123"
  • 숫자: 42, 3.14
  • 튜플(불변 항목만 포함하는 경우): (1, 2), ("x", "y")
  • 불리언: True, False
  • None

유효하지 않은 키 타입(가변, mutable):

  • 리스트: [1, 2, 3]는 키가 될 수 없습니다
  • 딕셔너리: {"a": 1}는 키가 될 수 없습니다
  • 세트: {1, 2, 3}는 키가 될 수 없습니다
python
# 유효한 키
valid_dict = {
    "name": "Alice",           # 문자열 키
    42: "answer",              # 정수 키
    3.14: "pi",                # 실수 키
    (1, 2): "coordinates",     # 튜플 키
    True: "yes",               # 불리언 키
    None: "nothing"            # None 키
}
print(valid_dict["name"])      # Output: Alice
print(valid_dict[42])          # Output: answer
print(valid_dict[(1, 2)])      # Output: coordinates
 
# WARNING: Invalid keys - for demonstration only
# invalid_dict = {[1, 2]: "list key"}  # PROBLEM: TypeError: unhashable type: 'list'
# invalid_dict = {{1, 2}: "set key"}   # PROBLEM: TypeError: unhashable type: 'set'

초보자가 흔히 하는 실수(Common Beginner Mistake): 자주 발생하는 오류 중 하나는 논리적으로 그럴듯해 보이기 때문에 리스트를 딕셔너리 키로 사용하려고 시도하는 것입니다. 예를 들어 위치 데이터를 저장하기 위해 좌표 [x, y]를 키로 쓰고 싶을 수 있습니다. Python이 TypeError: unhashable type: 'list'를 발생시키면 초보자들은 왜 그런지 종종 이해하지 못합니다. 리스트에 키로 쓰고 싶은 데이터가 그대로 들어 있는데 말이죠.

그 이유는 리스트가 가변(mutable) 타입(변경 가능)이기 때문이며, Python은 딕셔너리 키가 안정적이고 변하지 않기를 요구하기 때문입니다. 리스트처럼 생긴 무언가를 키로 써야 한다면, 먼저 튜플로 변환하세요: tuple([1, 2])(1, 2)가 되며 키로 사용할 수 있습니다. 튜플은 불변이라서 완벽하게 동작합니다:

python
# Wrong: 리스트를 키로 사용하려고 시도
# locations = {[0, 0]: "origin", [1, 0]: "east"}  # PROBLEM: TypeError
 
# Right: 튜플로 변환
locations = {(0, 0): "origin", (1, 0): "east", (0, 1): "north"}
print(locations[(0, 0)])  # Output: origin
print(locations[(1, 0)])  # Output: east

반면 값은 가변/불변 여부와 관계없이 어떤 타입이든 될 수 있습니다:

python
# 값은 어떤 타입이든 가능합니다
flexible_dict = {
    "numbers": [1, 2, 3],              # 리스트 값
    "nested": {"a": 1, "b": 2},        # 딕셔너리 값
    "function": len,                    # 함수 값
    "mixed": (1, [2, 3], {"x": 4})     # 가변 항목을 포함하는 튜플
}
print(flexible_dict["numbers"])        # Output: [1, 2, 3]
print(flexible_dict["nested"]["a"])    # Output: 1

해시 가능성에 대해서는 17장에서 더 깊게 살펴보겠지만, 지금은 다음만 기억하세요: 키로는 불변 타입(문자열, 숫자, 튜플)을 사용하면 문제없습니다.

16.2) 딕셔너리 항목 추가 및 업데이트

16.2.1) 새 키-값 쌍 추가하기

딕셔너리에 새 항목을 추가하는 것은 간단합니다. 새 키에 값을 대입하기만 하면 됩니다:

python
grades = {"Alice": 95, "Bob": 87}
print(grades)  # Output: {'Alice': 95, 'Bob': 87}
 
# 새 학생 추가
grades["Charlie"] = 92
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# 다른 학생 추가
grades["Diana"] = 88
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}

키가 존재하지 않으면 Python이 새로 만듭니다. 키가 이미 존재하면 Python은 값을 업데이트합니다(다음에서 다룹니다).

16.2.2) 기존 값 업데이트하기

값을 업데이트하려면, 기존 키에 새 값을 대입하면 됩니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Bob의 성적 업데이트
grades["Bob"] = 90
print(grades)  # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92}
 
# 여러 성적 업데이트
grades["Alice"] = 98
grades["Charlie"] = 94
print(grades)  # Output: {'Alice': 98, 'Bob': 90, 'Charlie': 94}

Python은 추가와 업데이트를 구분하지 않습니다. 문법이 동일합니다. 키가 존재하면 값이 업데이트되고, 없으면 새 항목이 생성됩니다.

다음은 추가와 업데이트를 모두 보여주는 실용적인 예시입니다:

python
# 재고 추적
inventory = {"apple": 50, "banana": 30}
print("Initial inventory:", inventory)  # Output: Initial inventory: {'apple': 50, 'banana': 30}
 
# 사과 재입고(기존 값 업데이트)
inventory["apple"] = inventory["apple"] + 20
print("After restocking apples:", inventory)  # Output: After restocking apples: {'apple': 70, 'banana': 30}
 
# 새 제품 추가(새 키 추가)
inventory["orange"] = 40
print("After adding oranges:", inventory)  # Output: After adding oranges: {'apple': 70, 'banana': 30, 'orange': 40}
 
# 바나나 일부 판매(기존 값 업데이트)
inventory["banana"] = inventory["banana"] - 10
print("After selling bananas:", inventory)  # Output: After selling bananas: {'apple': 70, 'banana': 20, 'orange': 40}

16.2.3) update()로 딕셔너리 병합하기

update() 메서드는 여러 키-값 쌍을 한 번에 추가하거나, 다른 딕셔너리를 현재 딕셔너리에 병합합니다:

python
grades = {"Alice": 95, "Bob": 87}
print(grades)  # Output: {'Alice': 95, 'Bob': 87}
 
# 여러 학생을 한 번에 추가
new_students = {"Charlie": 92, "Diana": 88}
grades.update(new_students)
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
 
# 기존 값 업데이트 및 새 항목 추가
more_updates = {"Bob": 90, "Eve": 85}  # Bob의 성적은 바뀌고, Eve는 새로 추가됩니다
grades.update(more_updates)
print(grades)  # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92, 'Diana': 88, 'Eve': 85}

update() 메서드는 딕셔너리를 제자리에서(in place) 수정합니다. 키가 이미 존재하면 값이 업데이트되고, 존재하지 않으면 키-값 쌍이 추가됩니다.

update()에 키워드 인자를 전달할 수도 있습니다:

python
student = {"name": "Alice", "age": 20}
print(student)  # Output: {'name': 'Alice', 'age': 20}
 
# 키워드 인자로 업데이트
student.update(age=21, major="Computer Science")
print(student)  # Output: {'name': 'Alice', 'age': 21, 'major': 'Computer Science'}

다음은 설정 값을 병합하는 실용적인 예시입니다:

python
# 기본 설정
config = {
    "theme": "light",
    "font_size": 12,
    "auto_save": True
}
 
# 사용자 선호(일부 기본값 덮어쓰기)
user_prefs = {
    "theme": "dark",
    "font_size": 14
}
 
# config에 user_prefs 병합
config.update(user_prefs)
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}

16.2.4) setdefault()로 키가 없을 때만 추가하기

setdefault() 메서드는 키가 아직 존재하지 않을 때만 키-값 쌍을 추가하고 싶을 때 유용합니다. 키가 이미 존재하면 값을 바꾸지 않고 현재 값을 반환합니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# Charlie 추가(키가 없음)
result = grades.setdefault("Charlie", 90)
print(result)  # Output: 90
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90}
 
# Alice 추가 시도(키가 존재 - 변화 없음)
result = grades.setdefault("Alice", 80)
print(result)  # Output: 95 (existing value returned)
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90} (unchanged)

이 방법은 사용자가 이미 커스터마이징한 값을 유지하면서, 필요한 모든 설정 키가 기본값과 함께 존재하도록 보장하고 싶을 때 특히 유용합니다:

python
# 기본값이 있는 애플리케이션 설정
config = {"theme": "light", "font_size": 12}
 
# 필요한 모든 설정이 기본값과 함께 존재하도록 보장
config.setdefault("auto_save", True)
config.setdefault("language", "en")
config.setdefault("theme", "dark")  # Won't change - already exists
 
print(config)
# Output: {'theme': 'light', 'font_size': 12, 'auto_save': True, 'language': 'en'}
 
# 이제 모든 설정에 안전하게 접근
print(f"Theme: {config['theme']}")           # Output: Theme: light
print(f"Auto-save: {config['auto_save']}")   # Output: Auto-save: True
print(f"Language: {config['language']}")     # Output: Language: en

16.3) del과 pop()으로 딕셔너리 항목 삭제하기

16.3.1) del로 항목 삭제하기

del 문은 딕셔너리에서 키-값 쌍을 제거합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
 
# Charlie 삭제
del grades["Charlie"]
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Diana': 88}
 
# 다른 학생 삭제
del grades["Bob"]
print(grades)  # Output: {'Alice': 95, 'Diana': 88}

존재하지 않는 키를 삭제하려고 하면 Python은 KeyError를 발생시킵니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# WARNING: KeyError - for demonstration only
# del grades["Charlie"]  # PROBLEM: KeyError: 'Charlie'

존재하지 않을 수도 있는 키를 안전하게 삭제하려면 먼저 확인하세요:

python
grades = {"Alice": 95, "Bob": 87}
 
# 안전한 삭제
if "Charlie" in grades:
    del grades["Charlie"]
else:
    print("Charlie not found")  # Output: Charlie not found

16.3.2) pop()으로 삭제하면서 값 가져오기

pop() 메서드는 키를 제거하고 그 값을 반환합니다. 항목을 제거하면서 그 값을 사용해야 할 때 유용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Bob의 성적을 제거하고 가져오기
bob_grade = grades.pop("Bob")
print(bob_grade)  # Output: 87
print(grades)  # Output: {'Alice': 95, 'Charlie': 92}
 
# 값을 제거하고 사용하기
charlie_grade = grades.pop("Charlie")
print(f"Charlie's final grade was {charlie_grade}")  # Output: Charlie's final grade was 92
print(grades)  # Output: {'Alice': 95}

del과 마찬가지로, pop()도 키가 존재하지 않으면 KeyError를 발생시킵니다. 하지만 대신 반환할 기본값을 제공할 수 있습니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# 기본값과 함께 pop(키가 없음)
diana_grade = grades.pop("Diana", 0)
print(diana_grade)  # Output: 0
print(grades)  # Output: {'Alice': 95, 'Bob': 87} (unchanged)
 
# 기본값과 함께 pop(키가 있음)
alice_grade = grades.pop("Alice", 0)
print(alice_grade)  # Output: 95
print(grades)  # Output: {'Bob': 87}

16.3.3) clear()로 모든 항목 제거하기

clear() 메서드는 딕셔너리의 모든 키-값 쌍을 제거하여 빈 상태로 만듭니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# 모든 항목 삭제
grades.clear()
print(grades)  # Output: {}
print(len(grades))  # Output: 0

이는 빈 딕셔너리로 재대입(grades = {})하는 것보다 더 명시적이며, 특히 다른 변수가 같은 딕셔너리를 참조할 때 중요합니다:

python
# 차이를 보여주는 예시
grades = {"Alice": 95, "Bob": 87}
backup = grades  # backup은 같은 딕셔너리를 참조합니다
 
# clear() 사용 - 두 변수 모두에 영향
grades.clear()
print(grades)   # Output: {}
print(backup)   # Output: {} (same dictionary was cleared)
 
# 다음 예시를 위해 초기화
grades = {"Alice": 95, "Bob": 87}
backup = grades
 
# 재대입 - grades에만 영향
grades = {}
print(grades)   # Output: {}
print(backup)   # Output: {'Alice': 95, 'Bob': 87} (different dictionary now)

이 동작은 18장에서 참조 의미론(reference semantics)을 다룰 때 더 살펴보겠지만, 지금은 다음을 기억하세요: clear()는 기존 딕셔너리를 비우고, 재대입은 새로운 빈 딕셔너리를 만듭니다.

16.4) 딕셔너리 뷰 객체: keys(), values(), items()

16.4.1) 딕셔너리 뷰 이해하기

딕셔너리는 세 가지 메서드를 제공하는데, 이들은 뷰 객체(view objects) 를 반환합니다. 뷰 객체는 딕셔너리의 키, 값 또는 키-값 쌍에 대한 동적 뷰를 제공하는 특별한 객체입니다. 이 뷰들은 딕셔너리 변경 사항을 자동으로 반영합니다:

  • keys(): 모든 키에 대한 뷰를 반환
  • values(): 모든 값에 대한 뷰를 반환
  • items(): 모든 키-값 쌍(튜플 형태)에 대한 뷰를 반환
python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 뷰 얻기
keys_view = grades.keys()
values_view = grades.values()
items_view = grades.items()
 
print(keys_view)    # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
print(values_view)  # Output: dict_values([95, 87, 92])
print(items_view)   # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])

이것들은 리스트가 아니라 뷰 객체입니다. 하지만 필요하다면 리스트로 변환할 수 있습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 뷰를 리스트로 변환
keys_list = list(grades.keys())
values_list = list(grades.values())
items_list = list(grades.items())
 
print(keys_list)    # Output: ['Alice', 'Bob', 'Charlie']
print(values_list)  # Output: [95, 87, 92]
print(items_list)   # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]

16.4.2) 뷰는 동적입니다

뷰 객체는 딕셔너리의 변경 사항을 자동으로 반영합니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# 뷰 생성
keys_view = grades.keys()
print(keys_view)  # Output: dict_keys(['Alice', 'Bob'])
 
# 딕셔너리 수정
grades["Charlie"] = 92
print(keys_view)  # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
 
# 항목 제거
del grades["Bob"]
print(keys_view)  # Output: dict_keys(['Alice', 'Charlie'])

이 동적 동작은 변경될 수 있는 딕셔너리 내용을 다뤄야 할 때 유용합니다. 하지만 바뀌지 않는 스냅샷이 필요하다면 뷰를 리스트로 변환하세요:

python
grades = {"Alice": 95, "Bob": 87}
 
# 스냅샷 생성
keys_snapshot = list(grades.keys())
print(keys_snapshot)  # Output: ['Alice', 'Bob']
 
# 딕셔너리 수정
grades["Charlie"] = 92
print(keys_snapshot)  # Output: ['Alice', 'Bob'] (unchanged)

동적 업데이트

영향 없음

딕셔너리

뷰 객체

리스트 스냅샷

딕셔너리 수정

16.4.3) keys() 사용하기

keys() 메서드는 딕셔너리의 모든 키에 대한 뷰를 반환합니다. 이는 어떤 키가 존재하는지 확인하거나 키를 순회(iteration)할 때 유용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 모든 키 가져오기
keys = grades.keys()
print(keys)  # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
 
# 키 존재 여부 확인
if "Alice" in keys:
    print("Alice is in the grade book")  # Output: Alice is in the grade book
 
# 키 개수 세기
print(f"Number of students: {len(keys)}")  # Output: Number of students: 3

keys()를 호출하지 않고도 딕셔너리 자체에서 멤버십 검사를 할 수도 있습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 둘은 동일합니다
if "Alice" in grades.keys():
    print("Found (using keys())")
 
if "Alice" in grades:
    print("Found (direct check)")  # 이쪽이 더 흔하고 간결합니다
 
# Output:
# Found (using keys())
# Found (direct check)

16.4.4) values() 사용하기

values() 메서드는 딕셔너리의 모든 값에 대한 뷰를 반환합니다. 키는 신경 쓰지 않고 값만 처리해야 할 때 유용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 모든 값 가져오기
values = grades.values()
print(values)  # Output: dict_values([95, 87, 92, 88])
 
# 통계 계산
total = sum(values)
count = len(values)
average = total / count
 
print(f"Total points: {total}")      # Output: Total points: 362
print(f"Number of students: {count}")  # Output: Number of students: 4
print(f"Average grade: {average}")   # Output: Average grade: 90.5
 
# 최고/최저 점수 찾기
print(f"Highest grade: {max(values)}")  # Output: Highest grade: 95
print(f"Lowest grade: {min(values)}")   # Output: Lowest grade: 87

16.4.5) items() 사용하기

items() 메서드는 키-값 쌍을 튜플 형태로 제공하는 뷰를 반환합니다. 키와 값을 모두 제공하기 때문에 가장 흔히 쓰이는 뷰입니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 모든 키-값 쌍 가져오기
items = grades.items()
print(items)  # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])
 
# 튜플을 명확히 보기 위해 리스트로 변환
items_list = list(items)
print(items_list)  # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]
 
# 개별 튜플 접근
first_item = items_list[0]
print(first_item)        # Output: ('Alice', 95)
print(first_item[0])     # Output: Alice
print(first_item[1])     # Output: 95

items() 뷰는 특히 순회에서 유용한데, 이는 다음 섹션에서 자세히 다룹니다. 미리 보기로 한 가지 예시를 보겠습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 각 키-값 쌍 처리
for name, grade in grades.items():
    print(f"{name}: {grade}")
 
# Output:
# Alice: 95
# Bob: 87
# Charlie: 92

16.5) 키, 값, 항목 순회하기

16.5.1) 키 순회하기(기본 동작)

딕셔너리를 for 반복문(loop)으로 직접 순회하면 키를 순회하게 됩니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 키 순회(암시적)
for name in grades:
    print(name)
 
# Output:
# Alice
# Bob
# Charlie

이는 grades.keys()를 순회하는 것과 같습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 키 순회(명시적)
for name in grades.keys():
    print(name)
 
# Output:
# Alice
# Bob
# Charlie

두 방식은 동일하게 동작합니다. .keys()를 생략한 암시적 버전이 더 흔하고 간결합니다.

다음은 키를 이용해 값에 접근하는 실용적인 예시입니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 합격한 학생 찾기(grade >= 90)
passing_students = []
for name in grades:
    if grades[name] >= 90:
        passing_students.append(name)
 
print("Students who passed:", passing_students)  # Output: Students who passed: ['Alice', 'Charlie']

16.5.2) 값 순회하기

값만 순회하려면 values()를 사용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 값 순회
for grade in grades.values():
    print(grade)
 
# Output:
# 95
# 87
# 92
# 88

이는 키가 무엇인지 상관하지 않고 값만 처리해야 할 때 유용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 총점과 평균 계산
total = 0
count = 0
for grade in grades.values():
    total = total + grade
    count = count + 1
 
average = total / count
print(f"Class average: {average}")  # Output: Class average: 90.5
 
# 모든 학생이 통과했는지 확인
all_passed = True
for grade in grades.values():
    if grade < 60:
        all_passed = False
        break
 
if all_passed:
    print("All students passed!")  # Output: All students passed!

16.5.3) items()로 키-값 쌍 순회하기

가장 흔하고 유용한 순회 패턴은 items()를 사용해 키와 값을 함께 가져오는 것입니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 키-값 쌍 순회
for name, grade in grades.items():
    print(f"{name} scored {grade}")
 
# Output:
# Alice scored 95
# Bob scored 87
# Charlie scored 92

여기서 튜플 언패킹(tuple unpacking)에 주목하세요: for name, grade in grades.items(). 각 항목은 ("Alice", 95) 같은 튜플이며, 이를 두 변수로 풀어 담습니다. 이는 튜플 인덱스를 직접 접근하는 것보다 훨씬 읽기 쉽습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 언패킹 없이(덜 읽기 좋음)
for item in grades.items():
    print(f"{item[0]} scored {item[1]}")
 
# 언패킹 사용(더 읽기 좋음)
for name, grade in grades.items():
    print(f"{name} scored {grade}")
 
# Both produce the same output:
# Alice scored 95
# Bob scored 87
# Charlie scored 92

16.5.4) 순회 중 딕셔너리 수정하기

경고: 순회하는 동안 딕셔너리의 크기(키 추가/삭제)를 변경하면 에러가 나거나 예상치 못한 동작이 발생할 수 있습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# WARNING: RuntimeError - for demonstration only
# for name in grades:
#     if grades[name] < 90:
#         del grades[name]  # PROBLEM: RuntimeError: dictionary changed size during iteration

최신 Python(3.7+)에서는 딕셔너리 크기를 바꾸려고 시도하는 즉시 RuntimeError가 발생합니다. Python은 수정이 일어난 것을 감지하고 예측 불가능한 동작을 막기 위해 실행을 중단합니다.

구버전 Python에서는 이 때문에 반복자가:

  • 처리해야 할 항목을 건너뛰거나
  • 같은 항목을 두 번 처리하거나
  • 일관되지 않은 결과를 내는

문제가 생길 수 있었습니다.

이 때문에 Python은 이제 명확한 에러 메시지로 빠르게 실패하도록 설계되어 있습니다.

순회 중에 딕셔너리를 수정해야 한다면, 키의 복사본을 순회하세요:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 안전: 키의 복사본을 순회
for name in list(grades.keys()):
    if grades[name] < 90:
        del grades[name]
 
print(grades)  # Output: {'Alice': 95, 'Charlie': 92}

또는 원하는 항목만 포함하는 새 딕셔너리를 만들 수 있습니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# 필터링된 항목만 가진 새 딕셔너리 만들기
high_grades = {}
for name, grade in grades.items():
    if grade >= 90:
        high_grades[name] = grade
 
print(high_grades)  # Output: {'Alice': 95, 'Charlie': 92}

두 번째 접근은 종종 더 명확하고 안전합니다. 35장에서는 딕셔너리 컴프리헨션(dictionary comprehensions)을 사용해 이를 더 우아하게 하는 방법도 배우게 됩니다.

16.6) 자주 쓰는 딕셔너리 메서드

16.6.1) in과 not in으로 키 확인하기

innot in 연산자는 딕셔너리에 키가 존재하는지 확인합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# 키 존재 여부 확인
if "Alice" in grades:
    print("Alice is in the grade book")  # Output: Alice is in the grade book
 
if "David" not in grades:
    print("David is not in the grade book")  # Output: David is not in the grade book

이는 값을 접근하기 전에 키 존재 여부를 확인하는 선호되는 방식입니다. get()을 사용하고 None을 검사하는 것보다 더 읽기 쉽고 Pythonic합니다:

python
grades = {"Alice": 95, "Bob": 87}
 
# 선호: in 사용
if "Alice" in grades:
    print(f"Alice's grade: {grades['Alice']}")  # Output: Alice's grade: 95
 
# 대안: get() 사용 후 None 검사
if grades.get("Alice") is not None:
    print(f"Alice's grade: {grades['Alice']}")  # Output: Alice's grade: 95

16.6.2) len()으로 항목 개수 얻기

len() 함수는 딕셔너리의 키-값 쌍 개수를 반환합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(len(grades))  # Output: 3
 
# 빈 딕셔너리
empty = {}
print(len(empty))  # Output: 0
 
# 수정 후
grades["Diana"] = 88
print(len(grades))  # Output: 4
 
del grades["Bob"]
print(len(grades))  # Output: 3

이는 딕셔너리가 비어 있는지 확인하거나 통계를 보고할 때 유용합니다:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
if len(grades) == 0:
    print("No students in grade book")
else:
    total = sum(grades.values())
    average = total / len(grades)
    print(f"{len(grades)} students, average grade: {average}")
    # Output: 4 students, average grade: 90.5

16.6.3) copy()로 딕셔너리 복사하기

copy() 메서드는 딕셔너리의 얕은 복사(shallow copy) 를 만듭니다. 즉, 같은 키-값 쌍을 가진 새 딕셔너리를 생성합니다:

python
original = {"Alice": 95, "Bob": 87}
duplicate = original.copy()
 
print(original)   # Output: {'Alice': 95, 'Bob': 87}
print(duplicate)  # Output: {'Alice': 95, 'Bob': 87}
 
# 복사본 수정
duplicate["Charlie"] = 92
print(original)   # Output: {'Alice': 95, 'Bob': 87} (unchanged)
print(duplicate)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}

이는 같은 딕셔너리를 참조(reference)하게 되는 단순 대입과는 다릅니다:

python
original = {"Alice": 95, "Bob": 87}
reference = original  # 복사가 아님 - 같은 딕셔너리
 
# reference를 통해 수정
reference["Charlie"] = 92
print(original)   # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92} (changed!)
print(reference)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}

얕은 복사와 깊은 복사(deep copy)는 18장에서 자세히 다룹니다. 지금은 다음만 기억하세요: 딕셔너리의 독립적인 복제본이 필요하면 copy()를 사용하세요.

16.6.4) | 연산자로 딕셔너리 병합하기(Python 3.9+)

Python 3.9에서는 딕셔너리를 병합하기 위한 | 연산자가 도입되었습니다. | 연산자는 두 딕셔너리의 모든 키를 결합한 새 딕셔너리를 만듭니다. 중복된 키의 경우 오른쪽 값이 왼쪽 값을 덮어씁니다. 원본 딕셔너리 둘 다 변경되지 않습니다.

python
defaults = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
 
# 딕셔너리 병합(user_prefs가 defaults를 덮어씀)
config = defaults | user_prefs
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}
 
# 원본 딕셔너리는 변경되지 않음
print(defaults)    # Output: {'theme': 'light', 'font_size': 12, 'auto_save': True}
print(user_prefs)  # Output: {'theme': 'dark', 'font_size': 14}

다음은 여러 소스에서 온 데이터를 결합할 때 이것이 어떻게 유용한지 보여주는 또 다른 예시입니다:

python
# 두 공급업체의 제품 정보
supplier_a = {
    "laptop": {"price": 999.99, "stock": 15},
    "mouse": {"price": 29.99, "stock": 50}
}
 
supplier_b = {
    "laptop": {"price": 949.99, "stock": 20},  # 더 좋은 가격과 재고
    "keyboard": {"price": 79.99, "stock": 30}
}
 
# 병합: 일치하는 제품에 대해 supplier_b 데이터가 supplier_a를 덮어씀
combined = supplier_a | supplier_b
print(combined)
# Output: {'laptop': {'price': 949.99, 'stock': 20}, 'mouse': {'price': 29.99, 'stock': 50}, 'keyboard': {'price': 79.99, 'stock': 30}}
 
# 이제 laptop은 supplier_b(더 좋은 조건), mouse는 supplier_a, keyboard는 supplier_b에서 가져온 상태입니다

|= 연산자는 제자리에서 병합합니다(왼쪽 딕셔너리를 수정합니다):

python
config = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
 
# 제자리 병합
config |= user_prefs
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}

이는 update()를 사용하는 것과 같지만 더 간결합니다:

python
config = {"theme": "light", "font_size": 12}
user_prefs = {"theme": "dark", "font_size": 14}
 
# 둘은 동일합니다
config.update(user_prefs)
# config |= user_prefs
 
print(config)  # Output: {'theme': 'dark', 'font_size': 14}

16.7) 구조화된 데이터를 위한 중첩 딕셔너리

16.7.1) 중첩 딕셔너리 만들기

왜 중첩 딕셔너리를 사용할까요? 학생 정보를 추적한다고 상상해 봅시다. ages = {"Alice": 20, "Bob": 19}, majors = {"Alice": "CS", "Bob": "Math"}, gpas = {"Alice": 3.8, "Bob": 3.6}처럼 별도의 딕셔너리를 만들 수도 있습니다. 하지만 이렇게 하면 다루기 불편해집니다. 세 딕셔너리를 동기화해야 하고, 한 학생의 모든 정보를 조회하려면 세 번 따로 조회해야 합니다. 중첩 딕셔너리는 관련 데이터를 한데 묶어 해결합니다. 학생 이름 하나가 학생의 모든 정보로 매핑되어, 한 번의 조회로 필요한 정보를 얻을 수 있습니다.

중첩 딕셔너리(nested dictionary) 는 값으로 다른 딕셔너리를 포함하는 딕셔너리입니다. 이는 구조화된 계층적 데이터를 표현하는 데 유용합니다:

python
# 여러 속성을 가진 학생 기록
students = {
    "Alice": {
        "age": 20,
        "major": "Computer Science",
        "gpa": 3.8
    },
    "Bob": {
        "age": 19,
        "major": "Mathematics",
        "gpa": 3.6
    },
    "Charlie": {
        "age": 21,
        "major": "Physics",
        "gpa": 3.9
    }
}
 
print(students)
# Output: {'Alice': {'age': 20, 'major': 'Computer Science', 'gpa': 3.8}, 'Bob': {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}, 'Charlie': {'age': 21, 'major': 'Physics', 'gpa': 3.9}}

각 학생 이름은 그 학생의 속성을 담고 있는 또 다른 딕셔너리로 매핑됩니다. 이 구조는 각 속성마다 별도의 딕셔너리를 사용하는 것보다 훨씬 유연하고 유지보수하기 쉽습니다.

students 딕셔너리

Alice

Bob

Charlie

age: 20

major: CS

gpa: 3.8

age: 19

major: Math

gpa: 3.6

age: 21

major: Physics

gpa: 3.9

16.7.2) 중첩 값에 접근하기

중첩 값에 접근하려면 대괄호를 여러 번 사용합니다:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
 
# 중첩 값 접근
alice_age = students["Alice"]["age"]
print(alice_age)  # Output: 20
 
bob_major = students["Bob"]["major"]
print(bob_major)  # Output: Mathematics
 
# 표현식에서 사용
print(f"Alice is {students['Alice']['age']} years old")
# Output: Alice is 20 years old
 
print(f"Bob's GPA: {students['Bob']['gpa']}")
# Output: Bob's GPA: 3.6

각 대괄호는 중첩의 한 단계에 접근합니다. 먼저 students["Alice"]가 내부 딕셔너리를 가져오고, 그다음 ["age"]가 그 딕셔너리에서 나이 값을 가져옵니다.

16.7.3) 중첩 값에 안전하게 접근하기

중첩 딕셔너리에 접근할 때는 각 단계가 존재하는지 확인해야 합니다:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science"},
    "Bob": {"age": 19}  # Bob은 전공을 선언하지 않았습니다
}
 
# 안전하지 않은 접근 - KeyError가 발생할 수 있음
# print(students["Bob"]["major"])  # PROBLEM: KeyError: 'major'
 
# 여러 번의 체크로 안전하게 접근
if "Bob" in students:
    if "major" in students["Bob"]:
        print(f"Bob's major: {students['Bob']['major']}")
    else:
        print("Bob hasn't declared a major")  # Output: Bob hasn't declared a major
 
# 안전한 중첩 접근을 위한 get() 사용
bob_major = students.get("Bob", {}).get("major", "Undeclared")
print(f"Bob's major: {bob_major}")  # Output: Bob's major: Undeclared

get() 체인은 "Bob"이 존재하지 않으면 첫 번째 get()이 빈 딕셔너리 {}를 반환하고, 그러면 두 번째 get()이 안전하게 "Undeclared"를 반환하기 때문에 동작합니다.

16.7.4) 중첩 딕셔너리 수정하기

중첩 딕셔너리에서도 항목을 추가, 업데이트, 삭제할 수 있습니다:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
 
# 중첩 값 업데이트
students["Alice"]["gpa"] = 3.9
print(f"Alice's new GPA: {students['Alice']['gpa']}")  # Output: Alice's new GPA: 3.9
 
# 기존 학생에 새 속성 추가
students["Bob"]["email"] = "bob@university.edu"
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6, 'email': 'bob@university.edu'}
 
# 중첩 데이터를 가진 새 학생 추가
students["Charlie"] = {
    "age": 21,
    "major": "Physics",
    "gpa": 3.7
}
print(f"Number of students: {len(students)}")  # Output: Number of students: 3
 
# 속성 제거
del students["Bob"]["email"]
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}

16.7.5) 중첩 딕셔너리 순회하기

중첩 루프를 사용해 중첩 딕셔너리를 순회할 수 있습니다:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
    "Charlie": {"age": 21, "major": "Physics", "gpa": 3.9}
}
 
# 학생과 그 속성을 순회
for name, info in students.items():
    print(f"\n{name}:")
    for key, value in info.items():
        print(f"  {key}: {value}")
 
# Output:
# Alice:
#   age: 20
#   major: Computer Science
#   gpa: 3.8
#
# Bob:
#   age: 19
#   major: Mathematics
#   gpa: 3.6
#
# Charlie:
#   age: 21
#   major: Physics
#   gpa: 3.9

다음은 특정 조건을 만족하는 학생을 찾는 실용적인 예시입니다:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
    "Charlie": {"age": 21, "major": "Physics", "gpa": 3.9},
    "Diana": {"age": 20, "major": "Computer Science", "gpa": 3.5}
}
 
# GPA가 3.7 이상인 CS 학생 찾기
print("High-performing CS students:")
for name, info in students.items():
    if info["major"] == "Computer Science" and info["gpa"] >= 3.7:
        print(f"  {name} (GPA: {info['gpa']})")
 
# Output:
# High-performing CS students:
#   Alice (GPA: 3.8)

딕셔너리는 Python에서 가장 강력하고 다재다능한 자료구조 중 하나입니다. 빠른 조회, 유연한 구성, 그리고 흔한 프로그래밍 문제에 대한 우아한 해결책을 제공합니다. Python 학습을 계속하다 보면 설정(config)부터 데이터 처리, 복잡한 애플리케이션 구축까지 어디에서나 딕셔너리를 만나게 될 것입니다.

이 장에서 다룬 패턴—세기(counting), 그룹화(grouping), 조회 테이블(lookup tables), 데이터 변환(data transformation)—은 Python에서 구조화된 데이터를 다루기 위한 기반이 됩니다. 다음 장에서는 딕셔너리를 보완하는 또 다른 컬렉션 타입인 세트(sets)를 살펴보며, 고유하고 순서 없는 데이터를 다루는 방법을 배웁니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai