Skip to content

Python collections 모듈: Counter, defaultdict, deque, namedtuple 가이드

Updated on

Python의 기본 데이터 구조(list, dict, tuple, set)는 대부분의 작업을 처리합니다. 하지만 코드가 장난감 예제를 넘어 커지기 시작하면, 이들의 한계에 부딪히게 됩니다. 요소를 세려면 수동으로 dict 루프를 돌려야 합니다. 데이터를 그룹핑하려면 if key not in dict 같은 체크가 코드 곳곳에 늘어납니다. list를 큐로 쓰면 앞에서 꺼낼 때 O(n) 비용을 치르게 됩니다. 구조화된 레코드를 그냥 tuple로 표현하면, 필드 접근이 읽기 어려운 “인덱스 맞추기 게임”이 됩니다. 각각은 작은 우회책처럼 보이지만, 금방 누적되어 코드 가독성을 떨어뜨리고 실행을 느리게 하며, 버그 가능성을 키웁니다.

Python 표준 라이브러리의 collections 모듈은 이러한 문제를 해결하기 위한 목적 지향 컨테이너 타입을 제공합니다. Counter는 한 번의 호출로 요소를 셉니다. defaultdict는 자동 기본값으로 KeyError를 없앱니다. deque는 시퀀스의 양 끝에서 O(1) 연산을 제공합니다. namedtuple은 완전한 클래스를 만들지 않고도 tuple에 필드 이름을 추가합니다. OrderedDictChainMap은 일반 dict로는 깔끔하게 표현하기 어려운 “순서”와 “계층적 조회” 패턴을 다룹니다.

이 가이드는 collections 모듈의 주요 클래스를 모두 다루며, 동작하는 코드, 성능 분석, 실무 패턴을 함께 제공합니다. 로그 파일 처리, 캐시 구축, 설정 레이어 관리, 데이터 파이프라인 구조화 등 어떤 작업을 하든, 이 컨테이너들은 코드를 더 짧고 빠르며 더 정확하게 만들어 줄 것입니다.

📚

collections 모듈 개요

collections 모듈은 Python의 범용 내장 컨테이너를 확장하는 특화 컨테이너 데이터 타입을 제공합니다.

import collections
 
# See all available classes
print([name for name in dir(collections) if not name.startswith('_')])
# ['ChainMap', 'Counter', 'OrderedDict', 'UserDict', 'UserList',
#  'UserString', 'abc', 'defaultdict', 'deque', 'namedtuple']
ClassPurposeReplaces
Counter해시 가능한 객체 개수 세기수동 dict 카운팅 루프
defaultdict자동 기본값을 갖는 dictdict.setdefault(), if key not in 체크
deque양방향 큐(끝단 O(1))큐/스택으로 쓰는 list
namedtuple이름 있는 필드를 가진 tuple일반 tuple, 단순 데이터 클래스
OrderedDict삽입 순서를 기억하는 dictdict(3.7 이전), 순서 기반 연산
ChainMap계층(dict 레이어) 조회수동 dict 병합

Counter: 요소 개수 세기

Counter는 해시 가능한 객체를 세기 위한 dict 하위 클래스입니다. 요소를 카운트에 매핑하고, 빈도 분석을 위한 메서드를 제공합니다.

Counter 생성하기

from collections import Counter
 
# From an iterable
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
word_count = Counter(words)
print(word_count)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
 
# From a string
letter_count = Counter('mississippi')
print(letter_count)
# Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})
 
# From a dictionary
inventory = Counter({'shirts': 25, 'pants': 15, 'hats': 10})
 
# From keyword arguments
stock = Counter(laptops=5, monitors=12)

most_common()과 빈도 순위

from collections import Counter
 
text = "to be or not to be that is the question"
words = Counter(text.split())
 
# Get the 3 most common words
print(words.most_common(3))
# [('to', 2), ('be', 2), ('or', 1)]
 
# Get all elements sorted by frequency
print(words.most_common())
# [('to', 2), ('be', 2), ('or', 1), ('not', 1), ('that', 1), ('is', 1), ('the', 1), ('question', 1)]
 
# Least common: reverse the list or slice from the end
print(words.most_common()[-3:])
# [('is', 1), ('the', 1), ('question', 1)]

Counter 산술 연산

Counter는 덧셈, 뺄셈, 교집합, 합집합을 지원하며 멀티셋처럼 동작합니다.

from collections import Counter
 
a = Counter(x=4, y=2, z=1)
b = Counter(x=1, y=3, z=5)
 
# Addition: combine counts
print(a + b)  # Counter({'z': 6, 'y': 5, 'x': 5})
 
# Subtraction: drops zero and negative results
print(a - b)  # Counter({'x': 3})
 
# Intersection (min of each)
print(a & b)  # Counter({'y': 2, 'x': 1, 'z': 1})
 
# Union (max of each)
print(a | b)  # Counter({'z': 5, 'x': 4, 'y': 3})

실용적인 Counter 패턴

from collections import Counter
 
# Word frequency analysis
log_entries = [
    "ERROR: disk full",
    "WARNING: high memory",
    "ERROR: disk full",
    "ERROR: timeout",
    "WARNING: high memory",
    "ERROR: disk full",
    "INFO: backup complete",
]
error_types = Counter(entry.split(":")[0].strip() for entry in log_entries)
print(error_types)
# Counter({'ERROR': 4, 'WARNING': 2, 'INFO': 1})
 
# Find unique elements (count == 1)
data = [1, 2, 3, 2, 1, 4, 5, 4]
unique = [item for item, count in Counter(data).items() if count == 1]
print(unique)  # [3, 5]
 
# Check if one collection is a subset of another (anagram check)
def is_anagram(word1, word2):
    return Counter(word1.lower()) == Counter(word2.lower())
 
print(is_anagram("listen", "silent"))  # True
print(is_anagram("hello", "world"))    # False

Counter를 더 깊게 다루는 내용은 전용 Python Counter guide를 참고하세요.

defaultdict: 자동 기본값

defaultdict는 누락된 키에 대해 factory 함수를 호출해 기본값을 제공하는 dict 하위 클래스입니다. KeyError와 방어적 체크 코드를 없애 줍니다.

Factory 함수

from collections import defaultdict
 
# int factory: default is 0
counter = defaultdict(int)
counter['apples'] += 1
counter['oranges'] += 3
print(dict(counter))  # {'apples': 1, 'oranges': 3}
 
# list factory: default is []
groups = defaultdict(list)
pairs = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'pea')]
for category, item in pairs:
    groups[category].append(item)
print(dict(groups))
# {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'pea']}
 
# set factory: default is set()
index = defaultdict(set)
words = [('file1', 'python'), ('file2', 'python'), ('file1', 'java'), ('file3', 'python')]
for filename, lang in words:
    index[lang].add(filename)
print(dict(index))
# {'python': {'file1', 'file2', 'file3'}, 'java': {'file1'}}

그룹핑 패턴

관련 데이터를 그룹핑하는 것은 defaultdict(list)의 가장 흔한 사용처입니다. 수동 방식과 비교해 보세요:

from collections import defaultdict
 
students = [
    ('Math', 'Alice'), ('Science', 'Bob'), ('Math', 'Charlie'),
    ('Science', 'Diana'), ('Math', 'Eve'), ('History', 'Frank'),
]
 
# Without defaultdict -- verbose and error-prone
groups_manual = {}
for subject, name in students:
    if subject not in groups_manual:
        groups_manual[subject] = []
    groups_manual[subject].append(name)
 
# With defaultdict -- clean and direct
groups = defaultdict(list)
for subject, name in students:
    groups[subject].append(name)
 
print(dict(groups))
# {'Math': ['Alice', 'Charlie', 'Eve'], 'Science': ['Bob', 'Diana'], 'History': ['Frank']}

중첩 defaultdict

각 레벨을 수동으로 초기화하지 않고도 다단계 데이터 구조를 만들 수 있습니다.

from collections import defaultdict
 
# Two-level nested defaultdict
def nested_dict():
    return defaultdict(int)
 
sales = defaultdict(nested_dict)
sales['2025']['Q1'] = 150000
sales['2025']['Q2'] = 175000
sales['2026']['Q1'] = 200000
print(sales['2025']['Q1'])  # 150000
print(sales['2024']['Q3'])  # 0 (auto-created, no KeyError)
 
# Arbitrary depth nesting with a recursive factory
def deep_dict():
    return defaultdict(deep_dict)
 
config = deep_dict()
config['database']['primary']['host'] = 'localhost'
config['database']['primary']['port'] = 5432
config['database']['replica']['host'] = 'replica.local'
print(config['database']['primary']['host'])  # localhost

커스텀 Factory 함수

from collections import defaultdict
 
# Lambda for custom defaults
scores = defaultdict(lambda: 100)  # Every student starts with 100
scores['Alice'] -= 5
scores['Bob'] -= 10
print(scores['Charlie'])  # 100 (new student gets default)
print(dict(scores))  # {'Alice': 95, 'Bob': 90, 'Charlie': 100}
 
# Named function for complex defaults
def default_user():
    return {'role': 'viewer', 'active': True, 'login_count': 0}
 
users = defaultdict(default_user)
users['alice']['role'] = 'admin'
print(users['bob'])  # {'role': 'viewer', 'active': True, 'login_count': 0}

더 많은 패턴은 Python defaultdict guide를 참고하세요.

deque: 양방향 큐

deque(“deck”이라고 발음)는 양 끝에서 O(1)로 append/pop을 제공합니다. list는 모든 요소를 이동해야 하므로 pop(0)insert(0, x)가 O(n)입니다. 시퀀스의 양 끝을 자주 만지는 워크로드에서는 deque가 정답입니다.

핵심 연산

from collections import deque
 
d = deque([1, 2, 3, 4, 5])
 
# O(1) operations on both ends
d.append(6)         # Add to right: [1, 2, 3, 4, 5, 6]
d.appendleft(0)     # Add to left:  [0, 1, 2, 3, 4, 5, 6]
 
right = d.pop()     # Remove from right: 6
left = d.popleft()  # Remove from left:  0
print(d)  # deque([1, 2, 3, 4, 5])
 
# Extend from both sides
d.extend([6, 7])          # Right extend: [1, 2, 3, 4, 5, 6, 7]
d.extendleft([-1, 0])     # Left extend (reversed): [0, -1, 1, 2, 3, 4, 5, 6, 7]

maxlen이 있는 bounded deque

maxlen을 설정하면, 제한을 넘겨 추가되는 요소는 반대편에서 자동으로 버려집니다. 슬라이딩 윈도우와 캐시에 완벽합니다.

from collections import deque
 
# Keep only the last 5 items
recent = deque(maxlen=5)
for i in range(10):
    recent.append(i)
 
print(recent)  # deque([5, 6, 7, 8, 9], maxlen=5)
 
# Sliding window average
def moving_average(iterable, window_size):
    window = deque(maxlen=window_size)
    for value in iterable:
        window.append(value)
        if len(window) == window_size:
            yield sum(window) / window_size
 
data = [10, 20, 30, 40, 50, 60, 70]
print(list(moving_average(data, 3)))
# [20.0, 30.0, 40.0, 50.0, 60.0]

회전(Rotation)

rotate(n)은 요소를 오른쪽으로 n칸 이동시킵니다. 음수는 왼쪽 회전입니다.

from collections import deque
 
d = deque([1, 2, 3, 4, 5])
 
d.rotate(2)   # Rotate right by 2
print(d)  # deque([4, 5, 1, 2, 3])
 
d.rotate(-3)  # Rotate left by 3
print(d)  # deque([2, 3, 4, 5, 1])

deque vs list 성능

from collections import deque
import time
 
# Benchmark: append/pop from left side
n = 100_000
 
# List: O(n) for each insert at position 0
start = time.perf_counter()
lst = []
for i in range(n):
    lst.insert(0, i)
list_time = time.perf_counter() - start
 
# Deque: O(1) for appendleft
start = time.perf_counter()
dq = deque()
for i in range(n):
    dq.appendleft(i)
deque_time = time.perf_counter() - start
 
print(f"List insert(0, x): {list_time:.4f}s")
print(f"Deque appendleft:  {deque_time:.4f}s")
print(f"Deque is {list_time / deque_time:.0f}x faster")
# Typical output:
# List insert(0, x): 1.2340s
# Deque appendleft:  0.0065s
# Deque is 190x faster
Operationlistdeque
append(x) (right)O(1) amortizedO(1)
pop() (right)O(1)O(1)
insert(0, x) / appendleft(x)O(n)O(1)
pop(0) / popleft()O(n)O(1)
access by index [i]O(1)O(n)
Memory per element더 낮음약간 더 높음

양 끝에서 빠른 연산이 필요하면 deque를 쓰세요. 인덱스로 빠른 랜덤 액세스가 필요하면 list가 더 적합합니다.

전체 가이드는 Python deque를 참고하세요.

namedtuple: 이름 있는 필드를 가진 tuple

namedtuple은 이름 있는 필드를 가진 tuple 하위 클래스를 만들어, 완전한 클래스를 정의하는 오버헤드 없이 코드 자체를 문서화합니다.

namedtuple 만들기

from collections import namedtuple
 
# Define a type
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
 
# Access by name or index
print(p.x)     # 3
print(p[1])    # 4
print(p)       # Point(x=3, y=4)
 
# Alternative field definition styles
Color = namedtuple('Color', 'red green blue')        # Space-separated string
Config = namedtuple('Config', 'host, port, database')  # Comma-separated string

일반 tuple 대신 namedtuple을 쓰는 이유

from collections import namedtuple
 
# Plain tuple: which index is what?
employee_tuple = ('Alice', 'Engineering', 95000, True)
print(employee_tuple[2])  # 95000 -- but what does index 2 mean?
 
# namedtuple: self-documenting
Employee = namedtuple('Employee', 'name department salary active')
employee = Employee('Alice', 'Engineering', 95000, True)
print(employee.salary)     # 95000 -- immediately clear
print(employee.department) # Engineering

주요 메서드

from collections import namedtuple
 
Employee = namedtuple('Employee', 'name department salary')
emp = Employee('Alice', 'Engineering', 95000)
 
# _replace: create a new instance with some fields changed (immutable)
promoted = emp._replace(salary=110000)
print(promoted)  # Employee(name='Alice', department='Engineering', salary=110000)
print(emp)       # Employee(name='Alice', department='Engineering', salary=95000)  -- unchanged
 
# _asdict: convert to OrderedDict (Python 3.8+ returns regular dict)
print(emp._asdict())
# {'name': 'Alice', 'department': 'Engineering', 'salary': 95000}
 
# _fields: get field names
print(Employee._fields)  # ('name', 'department', 'salary')
 
# _make: create from an iterable
data = ['Bob', 'Marketing', 85000]
emp2 = Employee._make(data)
print(emp2)  # Employee(name='Bob', department='Marketing', salary=85000)

기본값

from collections import namedtuple
 
# defaults parameter (Python 3.6.1+)
Connection = namedtuple('Connection', 'host port timeout', defaults=[5432, 30])
conn1 = Connection('localhost')               # port=5432, timeout=30
conn2 = Connection('db.example.com', 3306)    # timeout=30
conn3 = Connection('db.example.com', 3306, 60)
 
print(conn1)  # Connection(host='localhost', port=5432, timeout=30)
print(conn2)  # Connection(host='db.example.com', port=3306, timeout=30)

typing.NamedTuple 대안

타입 어노테이션과 더 클래스 같은 문법을 원한다면 typing.NamedTuple을 사용하세요:

from typing import NamedTuple
 
class Point(NamedTuple):
    x: float
    y: float
    label: str = "origin"
 
p = Point(3.0, 4.0, "A")
print(p.x, p.label)  # 3.0 A
 
# Still a tuple -- supports unpacking, indexing, iteration
x, y, label = p
print(f"({x}, {y})")  # (3.0, 4.0)

namedtuple vs dataclass

Featurenamedtupledataclass
기본적으로 불변(immutable)YesNo (frozen=True 필요)
메모리 사용량tuple와 동일(작음)더 큼(일반 클래스)
반복/언패킹Yes (tuple이므로)No (직접 메서드를 추가하지 않으면)
타입 어노테이션typing.NamedTuple내장 지원
메서드/프로퍼티서브클래싱 필요직접 지원
상속제한적완전한 클래스 상속
적합한 용도가벼운 데이터 레코드복잡한 가변 객체

OrderedDict: 순서가 중요한 dict 연산

Python 3.7부터 일반 dict도 삽입 순서를 보존합니다. 그렇다면 OrderedDict는 언제 여전히 필요할까요?

OrderedDict가 여전히 중요한 경우

from collections import OrderedDict
 
# 1. Equality considers order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2)  # True -- regular dicts ignore order in comparison
 
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2)  # False -- OrderedDict considers order
 
# 2. move_to_end() for reordering
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
od.move_to_end('a')           # Move 'a' to the end
print(list(od.keys()))  # ['b', 'c', 'a']
 
od.move_to_end('c', last=False)  # Move 'c' to the beginning
print(list(od.keys()))  # ['c', 'b', 'a']

OrderedDict로 LRU 캐시 만들기

from collections import OrderedDict
 
class LRUCache:
    def __init__(self, capacity):
        self.cache = OrderedDict()
        self.capacity = capacity
 
    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)  # Mark as recently used
        return self.cache[key]
 
    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # Remove oldest
 
cache = LRUCache(3)
cache.put('a', 1)
cache.put('b', 2)
cache.put('c', 3)
cache.get('a')       # Access 'a', moves it to end
cache.put('d', 4)    # Evicts 'b' (least recently used)
print(list(cache.cache.keys()))  # ['c', 'a', 'd']

ChainMap: 계층(dict 레이어) 조회

ChainMap은 여러 dict를 하나의 “뷰”로 묶어 조회합니다. 지정된 순서대로 각 dict를 검색해, 처음 발견되는 값을 반환합니다. 설정 레이어링, 스코프 변수 조회, 컨텍스트 관리에 이상적입니다.

기본 사용법

from collections import ChainMap
 
defaults = {'theme': 'light', 'language': 'en', 'timeout': 30}
user_prefs = {'theme': 'dark'}
session = {'language': 'fr'}
 
config = ChainMap(session, user_prefs, defaults)
 
# Lookup searches session -> user_prefs -> defaults
print(config['theme'])     # 'dark'    (from user_prefs)
print(config['language'])  # 'fr'      (from session)
print(config['timeout'])   # 30        (from defaults)

설정 레이어링

from collections import ChainMap
import os
 
# Real-world config pattern: CLI args > env vars > config file > defaults
defaults = {
    'debug': False,
    'log_level': 'WARNING',
    'port': 8080,
    'host': '0.0.0.0',
}
 
config_file = {
    'log_level': 'INFO',
    'port': 9090,
}
 
env_vars = {
    k.lower(): v for k, v in os.environ.items()
    if k.lower() in defaults
}
 
cli_args = {'debug': True}  # Parsed from argparse
 
config = ChainMap(cli_args, env_vars, config_file, defaults)
print(config['debug'])      # True (from cli_args)
print(config['log_level'])  # 'INFO' (from config_file)
print(config['host'])       # '0.0.0.0' (from defaults)

new_child()로 스코프 컨텍스트 만들기

from collections import ChainMap
 
# Simulating variable scoping (like nested function scopes)
global_scope = {'x': 1, 'y': 2}
local_scope = ChainMap(global_scope)
 
# Enter a new scope
inner_scope = local_scope.new_child()
inner_scope['x'] = 10  # Shadows global x
inner_scope['z'] = 30  # New local variable
 
print(inner_scope['x'])  # 10 (local)
print(inner_scope['y'])  # 2  (falls through to global)
print(inner_scope['z'])  # 30 (local)
 
# Exit scope -- original is unchanged
print(local_scope['x'])  # 1 (global still intact)

모든 컬렉션 타입 비교

TypeBase ClassMutableUse CaseKey Advantage
CounterdictYes요소 카운팅most_common(), 멀티셋 산술
defaultdictdictYes누락 키 자동 초기화KeyError 없음, factory 함수
deque--Yes양방향 큐양 끝 O(1), maxlen
namedtupletupleNo구조화된 데이터 레코드필드명 접근, 가벼움
OrderedDictdictYes순서 민감 dictmove_to_end(), 순서 포함 동등성
ChainMap--Yes계층 조회설정 레이어링, 스코프 컨텍스트

성능 벤치마크

Counter vs 수동 카운팅

from collections import Counter, defaultdict
import time
 
data = list(range(1000)) * 1000  # 1 million items, 1000 unique
 
# Method 1: Counter
start = time.perf_counter()
c = Counter(data)
counter_time = time.perf_counter() - start
 
# Method 2: defaultdict(int)
start = time.perf_counter()
dd = defaultdict(int)
for item in data:
    dd[item] += 1
dd_time = time.perf_counter() - start
 
# Method 3: Manual dict
start = time.perf_counter()
manual = {}
for item in data:
    manual[item] = manual.get(item, 0) + 1
manual_time = time.perf_counter() - start
 
print(f"Counter:         {counter_time:.4f}s")
print(f"defaultdict(int):{dd_time:.4f}s")
print(f"dict.get():      {manual_time:.4f}s")
# Typical: Counter ~0.03s, defaultdict ~0.07s, dict.get() ~0.09s

큐 연산에서 deque vs list

from collections import deque
import time
 
n = 100_000
 
# Simulate a FIFO queue: append right, pop left
# List
start = time.perf_counter()
q = list(range(n))
while q:
    q.pop(0)
list_queue_time = time.perf_counter() - start
 
# Deque
start = time.perf_counter()
q = deque(range(n))
while q:
    q.popleft()
deque_queue_time = time.perf_counter() - start
 
print(f"List pop(0):     {list_queue_time:.4f}s")
print(f"Deque popleft(): {deque_queue_time:.4f}s")
print(f"Deque is {list_queue_time / deque_queue_time:.0f}x faster")
# Typical: List ~2.5s, Deque ~0.004s -> ~600x faster

실무 예제

Counter로 로그 분석

from collections import Counter
from datetime import datetime
 
# Parse and analyze server logs
log_lines = [
    "2026-02-18 10:15:03 GET /api/users 200",
    "2026-02-18 10:15:04 POST /api/login 401",
    "2026-02-18 10:15:05 GET /api/users 200",
    "2026-02-18 10:15:06 GET /api/products 500",
    "2026-02-18 10:15:07 POST /api/login 200",
    "2026-02-18 10:15:08 GET /api/users 200",
    "2026-02-18 10:15:09 GET /api/products 500",
    "2026-02-18 10:15:10 POST /api/login 401",
]
 
# Count status codes
status_codes = Counter(line.split()[-1] for line in log_lines)
print("Status codes:", status_codes.most_common())
# [('200', 4), ('401', 2), ('500', 2)]
 
# Count endpoints
endpoints = Counter(line.split()[3] for line in log_lines)
print("Top endpoints:", endpoints.most_common(2))
# [('/api/users', 3), ('/api/login', 3)]
 
# Count error endpoints (status >= 400)
errors = Counter(
    line.split()[3] for line in log_lines
    if int(line.split()[-1]) >= 400
)
print("Error endpoints:", errors)
# Counter({'/api/login': 2, '/api/products': 2})

ChainMap으로 설정 관리

from collections import ChainMap
import json
 
# Multi-layer config system for a web application
def load_config(config_path=None, cli_overrides=None):
    # Layer 1: Hard-coded defaults
    defaults = {
        'host': '127.0.0.1',
        'port': 8000,
        'debug': False,
        'db_pool_size': 5,
        'log_level': 'WARNING',
        'cors_origins': ['http://localhost:3000'],
    }
 
    # Layer 2: Config file
    file_config = {}
    if config_path:
        with open(config_path) as f:
            file_config = json.load(f)
 
    # Layer 3: CLI overrides (highest priority)
    cli = cli_overrides or {}
 
    # ChainMap searches cli -> file_config -> defaults
    return ChainMap(cli, file_config, defaults)
 
# Usage
config = load_config(cli_overrides={'debug': True, 'port': 9000})
print(config['debug'])        # True (CLI override)
print(config['port'])         # 9000 (CLI override)
print(config['db_pool_size']) # 5    (default)
print(config['log_level'])    # WARNING (default)

deque로 최근 항목 캐시

from collections import deque
 
class RecentItemsTracker:
    """Track the N most recent unique items."""
 
    def __init__(self, max_items=10):
        self.items = deque(maxlen=max_items)
        self.seen = set()
 
    def add(self, item):
        if item in self.seen:
            # Move to front by removing and re-adding
            self.items.remove(item)
            self.items.append(item)
        else:
            if len(self.items) == self.items.maxlen:
                # Remove the oldest item from the set too
                oldest = self.items[0]
                self.seen.discard(oldest)
            self.items.append(item)
            self.seen.add(item)
 
    def get_recent(self):
        return list(reversed(self.items))
 
# Track recently viewed products
tracker = RecentItemsTracker(max_items=5)
for product in ['shoes', 'shirt', 'hat', 'shoes', 'jacket', 'belt', 'hat']:
    tracker.add(product)
 
print(tracker.get_recent())
# ['hat', 'belt', 'jacket', 'shoes', 'shirt']

namedtuple로 데이터 파이프라인 구성

from collections import namedtuple, Counter, defaultdict
 
# Define structured records
Transaction = namedtuple('Transaction', 'id customer product amount date')
 
transactions = [
    Transaction(1, 'Alice', 'Widget', 29.99, '2026-02-01'),
    Transaction(2, 'Bob', 'Gadget', 49.99, '2026-02-01'),
    Transaction(3, 'Alice', 'Widget', 29.99, '2026-02-03'),
    Transaction(4, 'Charlie', 'Gadget', 49.99, '2026-02-05'),
    Transaction(5, 'Alice', 'Gizmo', 19.99, '2026-02-07'),
    Transaction(6, 'Bob', 'Widget', 29.99, '2026-02-08'),
]
 
# Most popular products
product_count = Counter(t.product for t in transactions)
print("Popular products:", product_count.most_common())
# [('Widget', 3), ('Gadget', 2), ('Gizmo', 1)]
 
# Revenue by customer
revenue = defaultdict(float)
for t in transactions:
    revenue[t.customer] += t.amount
print("Revenue:", dict(revenue))
# {'Alice': 79.97, 'Bob': 79.98, 'Charlie': 49.99}
 
# Convert to DataFrame for visualization
import pandas as pd
df = pd.DataFrame(transactions, columns=Transaction._fields)
print(df.groupby('customer')['amount'].sum())

PyGWalker로 컬렉션 데이터 시각화하기

Counter, defaultdict, namedtuple로 데이터를 처리한 뒤에는 결과를 시각화하고 싶어지는 경우가 많습니다. PyGWalker (opens in a new tab)는 어떤 pandas DataFrame이든 Jupyter notebook에서 Tableau 스타일의 인터랙티브 시각화 UI로 바꿔 줍니다:

from collections import Counter
import pandas as pd
import pygwalker as pyg
 
# Process data with collections
log_data = ["ERROR", "WARNING", "ERROR", "INFO", "ERROR", "WARNING", "INFO", "INFO"]
counts = Counter(log_data)
 
# Convert to DataFrame
df = pd.DataFrame(counts.items(), columns=['Level', 'Count'])
 
# Launch interactive visualization
walker = pyg.walk(df)

이를 통해 필드를 드래그 앤 드롭하고, 차트를 만들고, 데이터를 필터링하며, 별도의 시각화 코드를 작성하지 않고도 패턴을 인터랙티브하게 탐색할 수 있습니다. 특히 Counterdefaultdict로 그룹핑 처리한 대규모 데이터의 분포를 시각적으로 확인할 때 유용합니다.

이러한 컬렉션 실험을 인터랙티브하게 실행하려면, RunCell (opens in a new tab)이 AI 기반 Jupyter 환경을 제공하여 즉각적인 피드백과 함께 데이터 처리 파이프라인을 반복 개선할 수 있습니다.

여러 컬렉션 타입 조합하기

collections의 진짜 힘은 여러 타입을 하나의 파이프라인에서 조합할 때 드러납니다.

from collections import Counter, defaultdict, namedtuple, deque
 
# Named record type
LogEntry = namedtuple('LogEntry', 'timestamp level message')
 
# Simulated log stream
log_stream = deque([
    LogEntry('10:01', 'ERROR', 'Connection timeout'),
    LogEntry('10:02', 'INFO', 'Request processed'),
    LogEntry('10:03', 'ERROR', 'Connection timeout'),
    LogEntry('10:04', 'WARNING', 'High memory'),
    LogEntry('10:05', 'ERROR', 'Disk full'),
    LogEntry('10:06', 'INFO', 'Request processed'),
    LogEntry('10:07', 'ERROR', 'Connection timeout'),
], maxlen=100)
 
# Count error types
error_counts = Counter(
    entry.message for entry in log_stream if entry.level == 'ERROR'
)
print("Error types:", error_counts.most_common())
# [('Connection timeout', 3), ('Disk full', 1)]
 
# Group entries by level
by_level = defaultdict(list)
for entry in log_stream:
    by_level[entry.level].append(entry)
 
for level, entries in by_level.items():
    print(f"{level}: {len(entries)} entries")
# ERROR: 4 entries
# INFO: 2 entries
# WARNING: 1 entries

FAQ

Python collections 모듈이란 무엇인가요?

collections 모듈은 Python 표준 라이브러리의 일부입니다. 내장 타입(dict, list, tuple, set)을 추가 기능으로 확장하는 특화 컨테이너 데이터 타입을 제공합니다. 주요 클래스는 Counter, defaultdict, deque, namedtuple, OrderedDict, ChainMap입니다. 각각은 내장 타입만으로는 비효율적인 특정 범주의 데이터 처리 문제를 더 효율적으로 해결합니다.

Counter와 defaultdict(int)는 언제 각각 사용해야 하나요?

주된 목적이 요소 카운팅이거나 빈도 분포를 비교하는 것이라면 Counter를 사용하세요. most_common(), 산술 연산자(+, -, &, |)를 제공하고, 생성자 한 번으로 전체 iterable을 바로 셀 수 있습니다. 반면 카운팅이 더 큰 데이터 구조 패턴의 일부이거나, 정수 기본값을 가진 범용 dict가 필요하다면 defaultdict(int)가 적합합니다.

Python에서 deque는 thread-safe인가요?

네. CPython에서 deque.append(), deque.appendleft(), deque.pop(), deque.popleft()는 GIL(Global Interpreter Lock) 덕분에 원자적(atomic) 연산입니다. 그래서 추가적인 락 없이도 thread-safe 큐로 사용할 수 있습니다. 다만 “확인 후 실행(check-then-act)” 같은 복합 연산은 여전히 명시적 동기화가 필요합니다.

namedtuple과 dataclass의 차이는 무엇인가요?

namedtuple은 이름 있는 필드를 가진 불변 tuple 하위 클래스를 만듭니다. 가볍고, 반복/언패킹을 지원하며, 메모리를 최소로 사용합니다. dataclass(Python 3.7+, dataclasses 모듈)는 기본적으로 가변 속성을 가진 완전한 클래스를 만들며, 메서드/프로퍼티/상속을 지원합니다. 단순하고 불변인 레코드에는 namedtuple, 가변성이나 복잡한 동작, 풍부한 타입 어노테이션이 필요하면 dataclass를 사용하세요.

Python 3.7+에서도 OrderedDict가 여전히 필요한가요?

네, 두 가지 경우에 특히 그렇습니다. 첫째, OrderedDict는 동등성 비교에서 순서를 고려합니다(OrderedDict(a=1, b=2) != OrderedDict(b=2, a=1)), 반면 일반 dict는 그렇지 않습니다. 둘째, OrderedDict는 요소 재정렬을 위한 move_to_end()를 제공하며, LRU 캐시나 우선순위 기반 자료구조에 유용합니다. 그 외 대부분의 경우에는 일반 dict로 충분하고 더 빠릅니다.

ChainMap은 dict 병합과 무엇이 다른가요?

ChainMap은 데이터를 복사하지 않고 여러 dict 위에 “뷰”를 만들어 줍니다. 조회는 지정된 순서대로 각 dict를 탐색하며, 기반 dict가 변경되면 ChainMap에도 즉시 반영됩니다. 반면 {**d1, **d2} 또는 d1 | d2 같은 병합은 모든 데이터를 복제한 새로운 dict를 만듭니다. ChainMap은 큰 dict에서 메모리 효율이 좋고, 설정/스코프 같은 “레이어 구조”를 유지할 수 있습니다.

collections 타입에 타입 힌트를 쓸 수 있나요?

네. 예를 들어 collections.Counter[str], collections.defaultdict[str, list[int]], collections.deque[int]처럼 사용할 수 있습니다. namedtuple에는 클래스 정의에서 타입 어노테이션을 직접 지원하는 typing.NamedTuple을 권장합니다. 모든 collections 타입은 mypy 등 타입 체커와 호환됩니다.

결론

Python의 collections 모듈은 흔한 보일러플레이트 패턴을 제거해 주는 6가지 특화 컨테이너 타입을 제공합니다. Counter는 수동 카운팅 루프를 대체합니다. defaultdictKeyError 처리를 없앱니다. deque는 빠른 양끝 연산을 제공합니다. namedtuple은 tuple에 읽기 좋은 필드 이름을 추가합니다. OrderedDict는 순서 민감 비교와 재정렬을 처리합니다. ChainMap은 데이터를 복제하지 않고 계층적 dict 조회를 관리합니다.

각 타입은 내장 컨테이너보다 특정 문제를 더 잘 해결합니다. 언제 어떤 타입을 써야 하는지 익히면 Python 코드는 더 짧고 빠르며 유지보수가 쉬워집니다. 핵심은 작업 패턴에 맞는 자료구조를 고르는 것입니다: 카운팅(Counter), 그룹핑(defaultdict), 큐/스택(deque), 구조화 레코드(namedtuple), 순서 기반 연산(OrderedDict), 계층 조회(ChainMap).

📚