왜 이 내용을 정리하나
로그 파일에서 사용자 ID별로 요청 기록을 모으거나, 판매 데이터에서 지역별 주문 목록을 나누는 일은 실무에서 자주 나온다. 같은 키를 가진 값들을 하나의 리스트로 모아 두면 이후에 개수를 세거나 합계를 내거나 정렬하기가 쉬워진다. 이런 작업은 반복문에서 키 존재 여부를 매번 검사하는 코드로도 할 수 있지만, 코드가 길어지고 실수가 생기기 쉽다. 그래서 collections 모듈의 defaultdict와 딕셔너리 컴프리헨션을 함께 쓰는 방법을 정리해 둘 필요가 있다.
이 노트에서는 defaultdict(list)로 키별 리스트를 만드는 방법, 그 결과를 딕셔너리 컴프리헨션으로 가공하는 방법, 그리고 리스트 길이로 빈도를 확인하는 방법을 정리해 본다.
개념 설명
일반 딕셔너리에서 없는 키에 값을 추가하려면 먼저 키가 있는지 확인하고, 없으면 빈 리스트를 만든 뒤 값을 넣어야 한다. defaultdict는 이 과정을 자동으로 처리한다. 생성할 때 기본값을 만드는 함수를 넘겨 두면, 아직 없는 키에 접근했을 때 그 함수를 호출해 기본값을 채운다. list를 넘기면 빈 리스트가 기본값이 된다.
defaultdict는 일반 딕셔너리의 하위 클래스이므로 items(), keys(), values() 같은 메서드를 그대로 쓸 수 있다. 다만 존재하지 않는 키를 조회하면 KeyError 대신 기본값이 만들어져 들어간다는 점이 다르다. 이 동작 때문에 단순 조회만 하려고 한 코드가 딕셔너리의 키를 늘려 버리는 경우가 있다. 조회만 필요하면 get 메서드를 쓰거나, 작업이 끝난 뒤 dict()로 일반 딕셔너리로 바꾸는 편이 안전하다.
딕셔너리 컴프리헨션은 {키식: 값식 for 항목 in 순회대상 if 조건} 형태로 새 딕셔너리를 한 줄에 만드는 문법이다. defaultdict로 모은 결과를 가공할 때 쓰면, 원하는 키만 골라내거나 리스트 길이를 값으로 바꾸는 일을 짧게 쓸 수 있다.
빈도 확인은 각 키에 연결된 리스트의 len()을 구하면 된다. 같은 값이 여러 번 들어 있는 리스트라면 collections.Counter를 한 번 더 써서 값별 개수를 셀 수도 있다. 이 노트에서는 키별 개수 확인을 중심으로 본다.
코드 예제
from collections import defaultdict
logs = [
('user_a', 'login'),
('user_b', 'view'),
('user_a', 'view'),
('user_c', 'login'),
('user_a', 'logout'),
('user_b', 'login'),
('user_b', 'view'),
]
# 1단계: 같은 사용자의 행동을 리스트로 모은다
grouped = defaultdict(list)
for user, action in logs:
grouped[user].append(action)
print('그룹 결과:')
for user, actions in grouped.items():
print(user, actions)
# 2단계: 딕셔너리 컴프리헨션으로 사용자별 행동 횟수를 구한다
count_by_user = {user: len(actions) for user, actions in grouped.items()}
print('사용자별 행동 횟수:', count_by_user)
# 3단계: 행동 횟수가 2회 이상인 사용자만 골라낸다
frequent_users = {user: n for user, n in count_by_user.items() if n >= 2}
print('2회 이상 행동한 사용자:', frequent_users)
# 4단계: 특정 행동이 몇 번 나왔는지 행동별로 센다
action_counts = {
action: sum(1 for a in actions if a == action)
for action in {'login', 'view', 'logout'}
for actions in [[a for u, a in logs]]
}
print('행동별 전체 횟수:', action_counts)
# 조회만 할 때는 일반 딕셔너리로 바꿔 두는 편이 안전하다
plain = dict(grouped)
print('일반 딕셔너리 키 목록:', sorted(plain.keys()))
이 코드를 실행하면 먼저 사용자별로 행동 리스트가 출력된다. user_a는 ['login', 'view', 'logout'], user_b는 ['view', 'login', 'view'], user_c는 ['login']으로 모인다. 이어서 사용자별 행동 횟수가 출력되고, 그중 2회 이상인 user_a와 user_b만 남은 딕셔너리가 출력된다. 마지막 행동별 전체 횟수 부분은 전체 행동 목록을 한 번 펼쳐서 각 행동이 몇 번 나왔는지 센다.
마지막 줄은 조회 후 일반 딕셔너리로 바꾸는 예다. 이렇게 해 두면 이후 코드에서 실수로 없는 키를 조회해도 새 키가 생기지 않는다.
헷갈리는 점
첫째, defaultdict에서 존재하지 않는 키를 대괄호로 조회하면 KeyError가 나지 않고 빈 리스트가 만들어져 들어간다. grouped['없는키']만 써도 딕셔너리의 키 수가 바뀐다는 뜻이다. 이 때문에 키 존재 여부를 확인할 때는 in 연산자를 쓰는 것이 안전하다. 'user_z' in grouped는 조회 과정에서 키를 만들지 않는다.
둘째, 딕셔너리 컴프리헨션 안에서 len()으로 개수를 세는 것과 Counter로 세는 것은 목적이 다르다. len()은 리스트 원소의 총 개수를 말하고, Counter는 같은 값이 몇 번 나왔는지를 말한다. 두 결과가 같아 보여도 값이 중복되는지에 따라 다르므로, 무엇을 세려는지 먼저 정해야 한다.
셋째, 컴프리헨션 안에서 같은 리스트를 여러 번 만들면 성능이 떨어진다. 위 예제의 4단계처럼 전체 목록을 매번 새로 만드는 방식은 데이터가 커지면 비효율적이다. 전체 목록을 한 번만 만들고 Counter로 세는 편이 낫다.
정리
defaultdict(list)를 쓰면 같은 키의 값을 리스트로 모으는 코드를 짧게 쓸 수 있다. 모은 결과는 딕셔너리 컴프리헨션으로 가공해 키별 개수나 조건에 맞는 항목만 추려 낼 수 있다. 빈도를 확인할 때는 리스트의 len()으로 총 개수를 구하고, 값별 빈도가 필요하면 Counter를 함께 쓴다. 조회 과정에서 키가 생기는 defaultdict의 특성은 in 연산자나 dict() 변환으로 피하는 것이 좋다.
이 노트에서는 defaultdict와 딕셔너리 컴프리헨션을 이용한 그룹화와 빈도 확인의 기본 흐름을 정리해 본다.
#파이썬 #defaultdict #딕셔너리컴프리헨션 #그룹화 #collections
'Python' 카테고리의 다른 글
| yield가 있는 함수 호출 시 본문 미실행과 제너레이터 객체 반환 확인하기 (inspect.isgeneratorfunction, type()) (0) | 2026.10.25 |
|---|---|
| Python 함수 기본 인자에 가변 리스트를 쓸 때 호출 사이에 값이 유지되는 이유와 확인 방법 (0) | 2026.10.24 |
| 제너레이터 표현식으로 파일 줄을 지연 평가하며 빈 줄과 주석 줄 건너뛰기 (0) | 2026.10.23 |
| Python 딕셔너리 컴프리헨션으로 리스트를 키-값 쌍으로 바꾸고 중복 키 동작 확인하기 (0) | 2026.10.23 |
| Python 함수 정의와 리스트 순회 기본 예제 정리 (0) | 2026.10.23 |