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

딕셔너리

리스트는 위치를 기준으로 값을 찾게 해줍니다. 하지만 이름으로 무언가를 찾고 싶을 때가 더 많습니다. "3번 항목을 줘"가 아니라 "지민의 점수를 알려줘" 같은 식으로요. 딕셔너리는 데이터를 키-값 쌍으로 저장합니다. 값을 찾을 때 위치가 아니라 키를 기준으로 찾는 것입니다.

리스트의 위치 인덱스가 의미를 갖지 않는 상황이라면 딕셔너리가 적합한 구조입니다. 딕셔너리는 임의의 키를 값에 매핑해서 O(1) 시간에 이름으로 값을 찾을 수 있게 해줍니다. 리더보드, JSON 응답, 설정 파일 등은 모두 키-값 매핑으로 자연스럽게 표현됩니다.

dict는 평균적으로 O(1) 시간에 조회, 삽입, 삭제가 이루어지는 키-값 저장소입니다(O(1)이라는 것은 딕셔너리가 얼마나 커지든 비용이 일정하게 유지된다는 뜻인데, 키를 해싱해서 곧바로 해당 슬롯을 가리키는 숫자로 바꾸기 때문입니다). 키는 해시 가능해야 하는데, 이는 내용이 절대 바뀌지 않아서 그 숫자가 안정적으로 유지된다는 뜻입니다. str, int, tuple은 해시 가능하지만 list는 그렇지 않습니다. 값에는 제약 없이 어떤 객체든 올 수 있습니다. Python 3.7부터는 딕셔너리가 삽입한 순서대로 키를 유지하는데, 이는 직렬화하거나 출력할 때 중요하게 작용합니다. 이런 구조는 여러분이 만든 객체의 속성(attribute)을 포함해서 파이썬 내부 곳곳에서 쓰이므로, 딕셔너리는 확실히 알아둘 가치가 있습니다. 리스트 챕터에서는 위치를 기준으로 하는 사촌 격 구조를 다룹니다.

딕셔너리 만들기

중괄호를 쓰고, 각 키와 값 사이에는 콜론을, 쌍 사이에는 쉼표를 넣습니다. 키는 거의 항상 문자열입니다. 값에는 숫자, 문자열, 다른 리스트, 심지어 다른 딕셔너리까지 무엇이든 올 수 있습니다.

딕셔너리 리터럴은 중괄호와 key: value 문법을 사용합니다. 키는 문자열, 정수, 튜플처럼 불변(해시 가능)한 타입이면 무엇이든 될 수 있습니다. 값은 어떤 파이썬 객체든 될 수 있습니다. 딕셔너리는 삽입 순서를 유지하므로, 순회할 때 추가한 순서대로 항목을 얻게 됩니다.

딕셔너리 리터럴은 왼쪽에서 오른쪽으로 평가됩니다. 키는 해시 가능해야 합니다(내용이 바뀌지 않아야 파이썬이 분류해 둔 숫자가 그대로 유지되기 때문입니다). str, int, tuple은 해당되지만 listdict는 해당되지 않습니다. 값에는 그런 제약이 없습니다. 삽입 순서는 Python 3.7부터 언어 차원에서 보장되므로, 순회와 직렬화 결과를 항상 똑같이 재현할 수 있습니다. 조용히 숨어 있는 함정 하나: 하나의 리터럴 안에 키가 중복되어도 오류가 나지 않고, 마지막 값만 남고 그 전 값은 사라집니다.

python
player = {
    "name": "지민",
    "score": 87,
    "level": 5,
    "alive": True,
}
Juno딕셔너리 만들기 중괄호를 쓰고, 각 키와 값 사이에 콜론을, 쌍 사이에 쉼표를 넣으면 됩니다. 키는 보통 문자열이고 값은 다른 딕셔너리까지 포함해서 뭐든 될 수 있어요. 넣은 순서 그대로 돌려받는다는 점을 생각보다 훨씬 많이 활용하게 되더라고요.
Juno딕셔너리 만들기 중괄호에 key: value 쌍을 넣는 방식입니다. 키는 str, int, tuple 같은 불변 타입이면 되고, 값은 어떤 객체든 상관없습니다. 순회는 삽입 순서를 따르니까, 순서가 중요한 상황에서도 딕셔너리를 써도 괜찮습니다.
Juno딕셔너리 만들기 키는 해시 가능해야 해서 str, int, tuple은 되고 list, dict는 안 됩니다. 삽입 순서는 3.7부터 보장되니 출력할 때 믿고 써도 됩니다. 중복 키가 있는 리터럴은 조심하세요. 경고 없이 조용히 마지막 값만 남습니다.

값 접근하기

값을 가져올 때는 대괄호에 키를 넣습니다. 키가 존재하지 않으면 파이썬은 KeyError를 발생시킵니다. 키가 있는지 확신할 수 없을 때는 .get()을 사용하세요. 오류로 멈추는 대신 None을 반환하거나, 지정한 기본값을 반환합니다.

대괄호로 접근하면 키가 없을 때 KeyError가 발생합니다. .get(key)는 키가 없으면 None을 반환합니다. .get(key, default)는 대신 지정한 기본값을 반환합니다. 키의 존재 여부가 불확실할 때는 .get()을 사용하세요. try/except로 접근을 감싸는 것보다 더 안전하고 읽기도 편합니다.

대괄호 접근은 키를 해싱해서 일치하는 슬롯을 읽는 방식으로, 평균적으로 O(1)입니다(크기와 무관하게 비용이 일정합니다). 키가 없으면 KeyError가 발생합니다. .get(key, default=None)은 같은 방식으로 조회하지만 키가 없을 때 오류 대신 기본값을 반환하므로, 키의 존재가 불확실할 때 적합한 선택입니다. 대괄호 접근 전에 미리 확인하려면 key in d도 O(1)입니다. 예/아니오만 필요하면 in을, 값이나 대체값을 한 번에 얻고 싶으면 .get()을 선택하세요.

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

player["name"]    # "지민"
player["score"]   # 87
player["lives"]   # KeyError (키가 존재하지 않음)
python
player.get("score")          # 87
player.get("lives")          # None (오류 없이 기본값인 None을 반환)
player.get("lives", 3)       # 3   (키가 없으면 이 기본값을 사용)

키가 없을 수도 있는 상황에서는 .get()이 더 안전합니다:

python
count = inventory.get("arrows", 0)   # 딕셔너리에 "arrows"가 없으면 0
Juno값 접근하기d["key"]는 값을 건네주지만 키가 없으면 KeyError를 일으킵니다. 확신이 없을 때는 .get()을 쓰세요. 오류로 멈추는 대신 None을 반환하거나 직접 넘긴 기본값을 반환합니다. 이제는 뭐든 기본값을 지정해두는 습관이 생겼는데, 그 뒤로 키가 없어서 프로그램이 멈추는 일이 없어졌어요.
Juno값 접근하기 대괄호 접근은 키가 없을 때 KeyError를 일으키고, .get(key)None을 반환하고, .get(key, default)는 넘긴 값을 그대로 반환합니다. 키가 없을 수도 있을 때는 .get()을 쓰세요. try/except로 감싸는 것보다 깔끔합니다.
Juno값 접근하기d[key].get() 모두 O(1)로 한 번 조회하는 것은 똑같고, 차이는 키가 없을 때 어떻게 되느냐입니다. 오류를 일으키는가, 기본값을 반환하는가의 차이죠. 예/아니오만 필요하면 key in d를, 값이나 대체값을 한 번에 얻고 싶으면 .get()을 쓰세요.

추가하고 수정하기

대괄호로 키에 값을 대입합니다. 키가 이미 있으면 값이 교체되고, 없으면 새 항목이 만들어집니다. 다른 딕셔너리 전체를 한 번에 합치고 싶다면 .update()를 사용하세요.

키에 값을 대입하는 것은 평균 O(1)이며, 두 가지 역할을 동시에 합니다. 키가 새로운 것이면 항목을 만들고, 이미 있으면 값을 교체합니다. .update()는 다른 딕셔너리나 키-값 쌍의 이터러블을 받아서 각 항목에 같은 대입을 적용하며, 기존 키는 덮어씁니다.

d[key] = value는 키를 해싱해서 평균 O(1) 단계로 삽입하거나 덮어씁니다. 그래서 별도의 "추가"와 "수정"이 따로 있는 게 아니라, 대입 하나가 둘 다 처리합니다. .update(other)other의 각 항목에 대해 같은 동작을 반복하며, 겹치는 키는 덮어씁니다. 원본을 건드리지 않고 합치고 싶다면 | 연산자(Python 3.9 이상)를 쓰면 됩니다. 새 딕셔너리를 반환하고 양쪽 입력은 그대로 두는 반면, |=.update()처럼 그 자리에서 값을 바꿉니다. 함수가 호출한 쪽이 여전히 들고 있는 딕셔너리를 건드리면 안 될 때는 |를 쓰세요.

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

player["score"] = 92        # 기존 값 수정
player["level"] = 5         # 새 키 추가
python
extras = {"level": 5, "alive": True}
player.update(extras)   # extras의 키로 추가하거나 덮어씀
Juno추가하고 수정하기 대괄호로 키에 값을 대입하세요. 이미 있으면 값이 교체되고, 없으면 새 항목이 생깁니다. 하나의 문법으로 둘 다 처리되니 미리 확인할 필요가 없어요. 다른 딕셔너리를 한 번에 합치고 싶다면 .update()를 쓰세요.
Juno추가하고 수정하기d[key] = value는 한 번의 동작으로 만들거나 교체하니, 추가와 수정이 따로 구분되지 않습니다. .update(other)는 딕셔너리 전체를 합치면서 겹치는 키는 덮어씁니다. 둘 다 키당 O(1)입니다.
Juno추가하고 수정하기 대입은 한 번의 O(1) 단계로 삽입하거나 덮어쓰고, .update()는 그 동작을 각 항목에 반복하는 것입니다. 원본을 건드리고 싶지 않을 때는 |가 새로 합쳐진 딕셔너리를 반환하고, |=는 그 자리에서 값을 바꿉니다. 함수 안에서는 |를 써서 호출한 쪽이 들고 있는 딕셔너리를 조용히 바꿔버리는 일을 피하세요.

항목 제거하기

항목을 제거하는 방법은 네 가지입니다. .pop()은 키를 제거하고 그 값을 돌려줍니다. 기본값을 지정한 .pop()은 키가 없을 수도 있을 때 안전합니다. del은 반환값 없이 키를 제거합니다. .clear()는 딕셔너리 전체를 비웁니다.

.pop(key)는 키가 없으면 KeyError를 일으킵니다. .pop(key, default)는 대신 기본값을 반환하므로 더 안전한 제거 방법입니다. del d[key]는 반환값 없이 키를 제거하고, 키가 없으면 KeyError를 일으킵니다. .clear()는 모든 항목을 제거하지만 딕셔너리 객체 자체는 그대로 남겨둡니다.

.pop(key, default)는 평균 O(1) 조회 한 번으로 제거와 반환을 동시에 처리하고, 기본값을 지정하면 키가 없어도 안전합니다. 기본값이 없으면 키가 없을 때 KeyError가 발생합니다. del d[key]는 반환값 없이 제거하고, 키가 없으면 오류가 발생합니다. .clear()는 딕셔너리를 비우지만 같은 객체는 그대로 유지되므로, 그 객체를 가리키는 다른 이름에서도 빈 딕셔너리로 보입니다. 기억해야 할 실패 사례가 있습니다. 순회 중에 딕셔너리를 변경하면 루프 도중에 RuntimeError가 발생합니다. 해결 방법은 for key in list(d):처럼 제거할 키 목록을 먼저 스냅샷으로 떠 두고, 그 목록을 기준으로 원본에서 제거하는 것입니다.

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

player.pop("level")            # "level"을 제거하고 5를 반환
player.pop("lives", None)      # 안전한 pop, 키가 없으면 None 반환
del player["score"]            # "score"를 제거, 반환값 없음
player.clear()                 # 전부 제거

기본값을 지정한 .pop()은 없을 수도 있는 키를 제거할 때 가장 안전한 방법입니다.

Juno항목 제거하기.pop(key)는 키를 제거하고 그 값을 돌려주고, .pop(key, None)은 키가 없어도 태연하게 넘어갑니다. del d[key]는 아무것도 반환하지 않고 제거하고, .clear()는 전부 비웁니다. .pop()에 기본값을 지정해 둔 덕분에 KeyError로 놀랄 일이 많이 줄었어요.
Juno항목 제거하기.pop(key)는 키가 없으면 오류를 일으키지만 .pop(key, default)는 그렇지 않아서 더 안전한 선택입니다. del d[key]는 반환값 없이 제거하며 키가 없으면 오류를 일으킵니다. .clear()는 딕셔너리를 비우지만 객체 자체는 그대로 유지합니다.
Juno항목 제거하기.pop(key, default)은 키가 없어도 안전하게 제거하고 값을 돌려주는 방법입니다. del과 대괄호 pop은 키가 없으면 오류를 일으킵니다. 정말 조심해야 할 것은 순회 중에 제거하면 RuntimeError가 발생한다는 점입니다. for key in list(d):로 먼저 스냅샷을 뜨고, 원본에서 삭제하세요.

순회하기

딕셔너리의 서로 다른 부분을 순회할 수 있게 해주는 세 가지 뷰가 있습니다. 딕셔너리를 그대로 순회하면 키를 얻습니다. .values()는 값을 줍니다. .items()는 둘 다 한 번에 주는데, 가장 많이 쓰게 될 방법입니다. 각 쌍을 두 개의 이름으로 풀어내면 루프가 깔끔하고 읽기 쉬워집니다.

.keys(), .values(), .items()는 리스트가 아니라 뷰 객체를 반환합니다. 뷰는 딕셔너리의 현재 상태를 동적으로 반영해서, 딕셔너리를 수정하면 뷰도 바로 갱신됩니다. .items()는 대부분의 루프에서 가장 유용한데, for k, v in d.items()처럼 튜플을 풀어 쓰면 코드가 명확하게 읽히기 때문입니다.

.keys(), .values(), .items()는 별도의 리스트가 아니라 딕셔너리를 그대로 들여다보는 뷰 객체를 반환합니다. 뷰는 아무것도 복사하지 않고 딕셔너리의 현재 상태를 그대로 반영하므로, 딕셔너리가 바뀌면 뷰도 함께 바뀝니다. 키는 유일하고 해시 가능하므로 키 뷰는 집합 연산을 지원합니다. d.keys() & other.keys()는 공통 키를 찾고, -는 차이를 찾는데, 두 설정을 비교할 때 편리합니다. 여기서도 순회 중 변경 문제는 똑같이 적용됩니다. 루프를 돌면서 딕셔너리를 반드시 바꿔야 한다면 list(d.items())로 스냅샷을 떠서 순회하세요.

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

for key in player:               # 키를 순회 (가장 흔한 방식)
    print(key)

for key in player.keys():        # 위와 동일, 명시적으로 키 뷰 사용
    print(key)

for value in player.values():    # 값들
    print(value)

for key, value in player.items():   # 둘 다, 가장 유용함
    print(f"{key}: {value}")

.items()가 가장 많이 쓰게 될 방법입니다. 각 쌍을 두 개의 이름으로 풀어내면 루프를 읽기 쉬워집니다.

Juno순회하기 딕셔너리를 그대로 순회하면 키를 얻습니다. .values()는 값을, .items()는 둘 다 한 번에 주는데, 가장 자주 쓰게 되는 방법이에요. for key, value in player.items()처럼 각 쌍을 두 이름으로 풀어 쓰니 루프가 훨씬 읽기 편해졌어요.
Juno순회하기.keys(), .values(), .items()는 리스트가 아니라 살아있는 뷰를 반환해서, 딕셔너리가 바뀌는 대로 함께 추적합니다. for k, v in d.items() 형태로 쓰는 .items()가 가장 명확하게 읽히고, 가장 자주 쓰게 될 루프입니다.
Juno순회하기 세 가지 뷰는 복사본이 아니라 살아있는 창 같은 존재라서, 딕셔너리가 바뀌는 그대로 반영합니다. 키 뷰는 집합 연산을 지원하므로 d.keys() & other.keys() 한 줄로 두 딕셔너리를 비교할 수 있습니다. 제거할 때와 같은 규칙이 적용됩니다. 순회 중에는 변경하지 말고, 꼭 필요하다면 list(d.items())로 스냅샷을 뜨세요.

멤버십 확인하기

in은 딕셔너리에 어떤 키가 존재하는지 확인합니다. 값이 아니라 키만 확인합니다. 존재하지 않는지 확인하려면 not in을 사용하세요.

innot in은 딕셔너리에서 O(1)이며, 키만 확인합니다. 값을 확인하려면 in d.values()를 쓸 수 있지만, 값은 인덱싱되어 있지 않으므로 O(n)이 걸립니다.

key in d는 키를 해싱해서 슬롯 하나만 읽으므로 평균 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()를 쓰면 되지만, 이런 경우는 거의 필요하지 않습니다.

Juno멤버십 확인하기in은 딕셔너리에 키가 있는지 알려주는데, 항상 키만이고 값은 절대 확인하지 않습니다. 딕셔너리가 아무리 커져도 속도는 그대로 유지됩니다. 무언가가 없는지 확인하고 싶으면 not in으로 바꿔 쓰세요.
Juno멤버십 확인하기innot in은 O(1)로 키만 확인합니다. 값을 확인하려면 value in d.values()가 필요한데, 값이 인덱싱되어 있지 않아서 O(n)입니다. 그러니 키로 찾도록 설계하는 게 좋습니다.
Juno멤버십 확인하기key in d는 O(1)이고 value in d.values()는 O(n)입니다. 이 차이가 바로 검색 기준을 키로 삼아야 하는 이유입니다. 계속 값을 훑고 있다면 딕셔너리의 방향이 잘못된 것이니, 역방향 매핑을 하나 추가하는 게 낫습니다.

딕셔너리 중첩하기

값 자체가 딕셔너리일 수도 있습니다. 여러 단계를 가진 구조화된 데이터, 예를 들어 통계 섹션이 있는 플레이어나 하위 섹션이 있는 설정 파일을 이런 식으로 표현합니다. 대괄호 두 쌍으로 중첩된 값에 접근합니다. 첫 번째는 바깥쪽 키를, 두 번째는 안쪽 키를 고릅니다.

중첩된 딕셔너리는 값 자체가 딕셔너리인 딕셔너리입니다. 대괄호를 연달아 붙여서 접근합니다. 안쪽 딕셔너리를 수정하면 바깥쪽 딕셔너리에도 영향이 미치는데, 바깥쪽 딕셔너리가 같은 객체에 대한 참조를 가지고 있기 때문입니다. 가능하면 중첩을 얕게 유지하세요. 깊게 중첩하면 금방 읽고 다루기 어려워집니다.

중첩된 딕셔너리는 안쪽 딕셔너리의 복사본이 아니라 참조를 가지고 있습니다(참조란 하나의 공유 객체를 가리키는 포인터이므로, 두 이름이 같은 안쪽 딕셔너리를 가리킬 수 있습니다). 여기서 복사가 문제를 일으킵니다. d.copy()얕은 복사라서 바깥쪽 딕셔너리는 복제되지만 안쪽 딕셔너리는 여전히 공유되므로, 복사본을 통해 값을 바꾸면 원본에도 그대로 반영됩니다. 완전히 독립된 복사본이 필요하다면 copy.deepcopy()가 트리 전체를 순회하며 모든 단계를 복제해 줍니다. 값을 바꿀 수도 있는 코드에 설정을 넘기기 전에 이 함수를 써두세요. 대괄호 접근은 단계마다 각각 O(1) 조회이므로, 깊이는 속도에는 아무 영향이 없고 가독성에만 영향을 줍니다.

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

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

대괄호를 연달아 붙여서 접근합니다. 구조가 깊게 중첩되면 다루기 번거로워지니, 가능하면 얕게 유지하세요.

Juno딕셔너리 중첩하기 값 자체가 통째로 하나의 딕셔너리일 수도 있는데, 통계 섹션이 있는 플레이어 같은 구조화된 데이터를 이런 식으로 담습니다. 대괄호 두 쌍으로 파고들어가는데, 첫 번째는 바깥쪽 키를, 두 번째는 그 안쪽에서 고릅니다. 편리하지만, 너무 깊게 중첩하지 않으려고 해요. 안 그러면 읽기가 번거로워지거든요.
Juno딕셔너리 중첩하기 중첩된 딕셔너리는 값이 딕셔너리인 딕셔너리이며, 대괄호를 연달아 붙여서 접근합니다. 바깥쪽 딕셔너리는 안쪽 딕셔너리에 대한 참조를 가지고 있으므로, 안쪽 딕셔너리를 바꾸면 참조되는 모든 곳에 나타납니다. 중첩을 얕게 유지하지 않으면 다루기 어려워집니다.
Juno딕셔너리 중첩하기 바깥쪽 딕셔너리는 안쪽 딕셔너리에 대한 참조를 저장하므로, .copy()는 얕은 복사입니다. 복사본도 그 안쪽 딕셔너리를 공유하기 때문에 한쪽을 바꾸면 양쪽 모두에 영향이 새어 나갑니다. 진짜로 독립된 복사본이 필요하면 copy.deepcopy()를 쓰세요. 깊이는 속도에는 부담이 없지만 가독성에는 비용이 크니 얕게 유지하세요.

setdefault

.setdefault()는 키가 있으면 그 값을 읽고, 없으면 기본값으로 설정한 뒤 그 값을 반환합니다. 키가 반드시 존재하게 만들면서도 이미 있는 값을 덮어쓰고 싶지 않을 때 유용합니다.

.setdefault(key, default)은 한 번의 호출로 읽거나 생성하는 동작을 합니다. 키가 있으면 아무것도 바꾸지 않고 현재 값을 반환하고, 없으면 기본값을 삽입한 뒤 그 값을 반환합니다. 대표적인 사용 사례는 존재 여부를 따로 확인하지 않고 그룹화된 구조를 쌓아 올리는 것입니다.

.setdefault(key, default)은 O(1) 조회 한 번으로 읽거나 생성합니다. 키가 있으면 기존 값을 그대로 반환하고, 없으면 default를 삽입한 뒤 반환합니다. 알아둘 만한 함정은 default가 일반적인 인자라서 키가 이미 있어도 항상 만들어진다는 점입니다. 그 기본값을 만드는 데 비용이 크다면(새 객체 생성, 함수 호출 등) 호출할 때마다 그 비용을 치르게 됩니다. 뒤에서 다룰 defaultdict는 미스가 났을 때만 팩토리를 실행해서 이 문제를 피해 갑니다. 흔히 쓰이는 "항목을 리스트로 그룹화하기" 패턴에는 .setdefault(key, []).append(...)key in d 확인을 대체하는 표준적인 한 줄짜리 코드입니다.

python
inventory = {}

inventory.setdefault("arrows", 0)    # "arrows": 0으로 설정하고 0을 반환
inventory.setdefault("arrows", 10)   # "arrows"가 이미 있어서 변경 없이 0을 반환

키 존재 여부를 먼저 확인하지 않고 그룹화된 구조를 쌓아 올릴 때 유용합니다:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)
Junosetdefault.setdefault(key, default)은 키가 있으면 읽고, 없으면 기본값으로 설정한 뒤 반환합니다. 이미 들어 있는 값을 망치지 않으면서 키가 존재하도록 만드는 깔끔한 방법이에요. 저는 주로 그룹화할 때 쓰는데, "이 키가 있나?" 하는 확인이 따로 필요 없어요.
Junosetdefault.setdefault(key, default)은 기존 값이 있으면 그대로 반환하고, 없으면 기본값을 삽입한 뒤 반환합니다. 그룹화할 때 특히 유용한데, d.setdefault(k, []).append(x)는 별도의 존재 확인 없이 키별로 리스트를 쌓아 올립니다.
Junosetdefault O(1) 조회 한 번으로 읽거나 생성하지만, 기본값은 키가 있어도 항상 만들어지므로 비용이 큰 기본값이라면 호출마다 그 대가를 치릅니다. 바로 이 부분을 defaultdict의 미스 시 팩토리 실행 방식이 해결해 줍니다. 그룹화할 때는 .setdefault(k, []).append(x)key in d 확인을 대체하는 한 줄짜리 방법입니다.

collections.defaultdict와 Counter

표준 라이브러리에는 흔한 패턴을 자동으로 처리해주는 딕셔너리 서브클래스가 두 가지 있습니다. defaultdict는 없는 키에 대해 기본값을 만들어주므로 KeyError를 겪을 일이 없습니다. Counter는 시퀀스에서 각 항목이 몇 번 나타나는지 세어서 딕셔너리 형태로 결과를 돌려줍니다.

defaultdict는 새 키에 대한 기본값을 만들어주는 콜러블을 받아서, .setdefault()가 필요 없게 해줍니다. Counter는 빈도수를 세는 데 특화된 딕셔너리로 .most_common() 메서드를 가지고 있습니다. 둘 다 딕셔너리의 서브클래스이므로 표준 딕셔너리 연산을 그대로 사용할 수 있습니다.

defaultdict(factory)는 없는 키를 처음 읽을 때 팩토리(listint처럼 인자가 없는 콜러블)를 실행해서 그 결과를 저장하고 반환하므로, d[k]KeyError를 일으키는 일이 없습니다. .setdefault()와의 핵심 차이는 팩토리가 미스가 났을 때만 실행된다는 점입니다. 그래서 비용이 큰 기본값이라도 일반적인 경로에서는 전혀 비용이 들지 않습니다. 주의할 함정 하나는, 없는 키를 그냥 읽기만 해도 그 키가 생성되므로 d[k]를 확인만 하는 것도 딕셔너리를 키울 수 있다는 점입니다. 삽입 없이 확인만 하고 싶다면 k in d를 쓰세요. Counter는 개수를 세는 데 특화된 dict입니다. 시퀀스를 넣으면 등장 횟수를 세고, .most_common(n)은 개수 기준 상위 n개를 반환합니다. 둘 다 collections 모듈에 있습니다.

defaultdictCounter는 표준 라이브러리에 있으므로 먼저 임포트해야 합니다. 임포트는 모듈 챕터에서 자세히 다룹니다.

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # team이 새로 생겨도 KeyError 없음
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를 쓰면 "반복문으로 개수 세기" 같은 상용구 코드를 많이 줄일 수 있습니다.

Junocollections.defaultdict와 Counterdefaultdict는 없는 키에 대해 기본값을 채워주니까, 그룹화나 카운팅을 할 때 더 이상 KeyError를 겪지 않습니다. Counter는 시퀀스에서 각 항목이 몇 번 나오는지 세어서 딕셔너리로 돌려줍니다. 손으로 짠 카운팅 루프를 처음 Counter로 바꿔봤을 때, 코드가 절반으로 줄었어요.
Junocollections.defaultdict와 Counterdefaultdict(list)defaultdict(int)는 새 키에 대해 기본값을 자동으로 만들어줘서, 그룹화나 카운팅에 쓰던 .setdefault()를 대체합니다. Counter는 빈도수에 맞춰진 딕셔너리로, .most_common(n)이 기본으로 들어 있습니다. 둘 다 딕셔너리의 서브클래스라서 여러분이 알고 있는 딕셔너리 지식이 그대로 통합니다.
Junocollections.defaultdict와 Counterdefaultdict의 팩토리는 미스가 났을 때만 실행되므로, .setdefault()와 달리 비용이 큰 기본값도 일반적인 경로에서는 전혀 비용이 들지 않습니다. 함정은 없는 키를 읽기만 해도 그 키가 생성된다는 점이니, d[k] 대신 k in d로 확인하세요. Counter는 시퀀스를 세고 .most_common(n)이 상위 n개를 뽑아줍니다.

실전 활용

점수 트래커를 만들고 모든 항목에 대한 요약을 출력해 보겠습니다:

python
scores = {"지민": 87, "서준": 74, "수아": 92, "도윤": 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   # 실제 파일 크기 대신 사용하는 자리표시자
    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": "alice", "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")

# 특성 중요도 값들을 합이 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}")