[Python 100일 챌린지] Day 68 - 고급 컬렉션 (collections 모듈)
collections 모듈은 데이터 구조의 스위스 아미 나이프예요! 🔧🎒
리스트와 딕셔너리만 알고 계셨나요? collections 모듈에는 Counter로 빈도수를 순식간에 세고, defaultdict로 KeyError 걱정 없이 코딩하고, deque로 양쪽에서 빠르게 데이터를 추가/제거할 수 있는 강력한 도구들이 가득해요! Pandas, NumPy 같은 데이터 분석 라이브러리들도 이런 고급 컬렉션을 활발히 사용하고 있답니다. 😊
데이터를 더 효율적으로 다루는 방법, 지금 배워봐요!
(35분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Phase 2: 리스트, 딕셔너리, 튜플
- Phase 4: 클래스 기초
🎯 학습 목표 1: namedtuple로 구조화된 데이터 다루기
한 줄 설명
namedtuple = 이름이 있는 튜플 📦🏷️
“점의 x좌표, y좌표”처럼 의미 있는 이름으로 데이터에 접근할 수 있어요!
1.1 namedtuple의 필요성
1
2
3
4
5
6
7
8
9
10
# 일반 튜플: 인덱스로만 접근
point = (10, 20)
print(point[0], point[1]) # 10 20 (무엇을 의미하는지 불명확)
# namedtuple: 이름으로 접근
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
point = Point(10, 20)
print(point.x, point.y) # 10 20 (명확!)
💡 실생활 비유: namedtuple은 주소에 건물 이름을 붙이는 것 같아요! “3번째 건물”보다 “스타벅스”라고 하면 훨씬 알아보기 쉽죠! 인덱스 대신 이름으로 접근하면 코드 가독성이 엄청 좋아져요!
namedtuple이 왜 좋을까요? 🌟
- 코드가 자기 설명적이 돼요 (self-documenting)
- 인덱스 실수를 방지해요
- 튜플처럼 불변이라 안전해요!
1.2 기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections import namedtuple
# 정의
Person = namedtuple('Person', ['name', 'age', 'city'])
# 생성
alice = Person('Alice', 25, 'Seoul')
bob = Person(name='Bob', age=30, city='Busan')
# 접근
print(alice.name) # Alice
print(alice.age) # 25
print(alice[0]) # Alice (인덱스 접근도 가능)
# 언패킹
name, age, city = alice
print(f"{name}님은 {age}세, {city}에 거주")
1.3 namedtuple의 장점
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import namedtuple
# 1. 불변(immutable)
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# p.x = 10 # ❌ AttributeError
# 2. 메모리 효율적
class PointClass:
def __init__(self, x, y):
self.x = x
self.y = y
import sys
p_tuple = Point(1, 2)
p_class = PointClass(1, 2)
print(sys.getsizeof(p_tuple)) # 56 bytes
print(sys.getsizeof(p_class)) # 48 bytes + __dict__
# 3. 딕셔너리 변환
print(p_tuple._asdict()) # {'x': 1, 'y': 2}
# 4. 값 교체 (_replace)
p2 = p_tuple._replace(x=10)
print(p2) # Point(x=10, y=2)
1.4 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from collections import namedtuple
# 학생 데이터
Student = namedtuple('Student', ['name', 'grade', 'major'])
students = [
Student('Alice', 85, 'CS'),
Student('Bob', 92, 'Math'),
Student('Charlie', 78, 'Physics')
]
# 정렬
sorted_students = sorted(students, key=lambda s: s.grade, reverse=True)
for student in sorted_students:
print(f"{student.name}: {student.grade}점")
# 필터링
cs_students = [s for s in students if s.major == 'CS']
print(cs_students)
실무에서 이렇게 써요! 💼
- API 응답 데이터를 구조화해서 저장
- CSV/JSON 파일에서 읽은 데이터를 namedtuple로 변환
- 데이터베이스 쿼리 결과를 namedtuple로 매핑
💡 핵심: namedtuple은 클래스보다 가볍고, 딕셔너리보다 메모리 효율적이에요! 불변 데이터를 다룰 때 최고예요!
🎯 학습 목표 2: Counter로 빈도수 계산하기
한 줄 설명
Counter = 자동 빈도수 계산기 🔢📊
“사과 3개, 바나나 2개…” 이런 걸 자동으로 세주는 마법 같은 도구예요!
2.1 기본 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections import Counter
# 리스트에서 빈도수 계산
fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
counter = Counter(fruits)
print(counter) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(counter['apple']) # 3
print(counter['grape']) # 0 (없는 키는 0 반환)
# 문자열에서 문자 빈도수
text = "hello world"
char_count = Counter(text)
print(char_count) # Counter({'l': 3, 'o': 2, ...})
💡 실생활 비유: Counter는 슈퍼마켓 계산대 같아요! 카트에 담긴 물건을 자동으로 세고, “사과 3개, 우유 2개” 이렇게 정리해주죠!
Counter가 왜 편할까요? 🎯
- 수동으로 세는 것보다 훨씬 빨라요
- 코드가 한 줄로 끝나요!
- 가장 빈번한 항목도 쉽게 찾아요!
2.2 유용한 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections import Counter
counter = Counter(['a', 'b', 'c', 'a', 'b', 'a'])
# most_common: 가장 빈번한 요소
print(counter.most_common(2)) # [('a', 3), ('b', 2)]
# elements: 모든 요소 반복자
print(list(counter.elements())) # ['a', 'a', 'a', 'b', 'b', 'c']
# update: 카운트 추가
counter.update(['a', 'd'])
print(counter) # Counter({'a': 4, 'b': 2, 'c': 1, 'd': 1})
# subtract: 카운트 빼기
counter.subtract(['a', 'b'])
print(counter) # Counter({'a': 3, 'b': 1, 'c': 1, 'd': 1})
2.3 Counter 연산
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import Counter
c1 = Counter(['a', 'b', 'c', 'a', 'b'])
c2 = Counter(['b', 'c', 'd', 'c'])
# 덧셈
print(c1 + c2) # Counter({'b': 3, 'c': 3, 'a': 2, 'd': 1})
# 뺄셈
print(c1 - c2) # Counter({'a': 2, 'b': 1})
# 교집합 (최소값)
print(c1 & c2) # Counter({'b': 1, 'c': 1})
# 합집합 (최대값)
print(c1 | c2) # Counter({'a': 2, 'b': 2, 'c': 2, 'd': 1})
2.4 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import Counter
# 단어 빈도수 분석
text = """
Python is a great programming language.
Python is easy to learn.
Programming in Python is fun.
"""
words = text.lower().split()
word_count = Counter(words)
# 가장 빈번한 단어 5개
print("가장 빈번한 단어:")
for word, count in word_count.most_common(5):
print(f" {word}: {count}번")
# 투표 집계
votes = ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob', 'Alice']
vote_count = Counter(votes)
winner = vote_count.most_common(1)[0]
print(f"\n당선자: {winner[0]} ({winner[1]}표)")
실무에서 이렇게 써요! 💼
- 텍스트 분석 (단어 빈도수)
- 투표 집계
- 로그 분석 (이벤트 빈도수)
- 데이터 마이닝
💡 핵심: Counter는 빈도수 계산의 필수 도구예요! 데이터 분석할 때 없어서는 안 될 친구랍니다!
🎯 학습 목표 3: defaultdict와 OrderedDict 활용하기
한 줄 설명
defaultdict = KeyError 걱정 끝! 🔑✨
없는 키에 접근해도 자동으로 기본값을 만들어주는 똑똑한 딕셔너리예요!
3.1 defaultdict: 기본값이 있는 딕셔너리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import defaultdict
# 일반 딕셔너리의 문제
normal_dict = {}
# normal_dict['key'] += 1 # ❌ KeyError
# defaultdict 사용
dd = defaultdict(int) # 기본값: 0
dd['key'] += 1
print(dd['key']) # 1
# 리스트 기본값
dd_list = defaultdict(list)
dd_list['fruits'].append('apple')
dd_list['fruits'].append('banana')
print(dd_list) # defaultdict(<class 'list'>, {'fruits': ['apple', 'banana']})
💡 실생활 비유: defaultdict는 무한 리필 음료수 같아요! 컵이 비어 있으면 자동으로 채워지죠. 없는 키에 접근하면 자동으로 기본값이 채워져요!
defaultdict이 왜 편할까요? 🌟
- KeyError 예외 처리 필요 없어요!
- 코드가 훨씬 간결해져요
- 그룹화 작업이 아주 쉬워요!
3.2 defaultdict 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import defaultdict
# 그룹화
students = [
('Alice', 'Math'),
('Bob', 'CS'),
('Charlie', 'Math'),
('David', 'CS')
]
groups = defaultdict(list)
for name, major in students:
groups[major].append(name)
print(dict(groups))
# {'Math': ['Alice', 'Charlie'], 'CS': ['Bob', 'David']}
# 카운팅
text = "hello world"
char_count = defaultdict(int)
for char in text:
char_count[char] += 1
print(dict(char_count))
3.3 OrderedDict (Python 3.7+에서는 일반 dict도 순서 유지)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import OrderedDict
# 순서 보장
od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3
print(od) # OrderedDict([('a', 1), ('b', 2), ('c', 3)])
# 순서 변경
od.move_to_end('a') # 'a'를 맨 뒤로
print(od) # OrderedDict([('b', 2), ('c', 3), ('a', 1)])
od.move_to_end('b', last=False) # 'b'를 맨 앞으로
print(od) # OrderedDict([('b', 2), ('c', 3), ('a', 1)])
💡 참고: Python 3.7+부터는 일반 dict도 순서를 유지해요! 하지만 OrderedDict는 순서를 변경하는 특별한 메서드들이 있어요!
🎯 학습 목표 4: deque와 ChainMap 마스터하기
한 줄 설명
deque = 양쪽에서 빠른 추가/제거 ⚡🔄
리스트는 앞쪽이 느린데, deque는 앞뒤 양쪽에서 모두 빨라요!
4.1 deque: 양방향 큐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections import deque
# 기본 사용
dq = deque([1, 2, 3])
# 양쪽 끝에서 추가/제거
dq.append(4) # 오른쪽 추가: [1, 2, 3, 4]
dq.appendleft(0) # 왼쪽 추가: [0, 1, 2, 3, 4]
dq.pop() # 오른쪽 제거: 4
dq.popleft() # 왼쪽 제거: 0
print(dq) # deque([1, 2, 3])
# 회전
dq.rotate(1) # 오른쪽으로 1칸 회전: [3, 1, 2]
dq.rotate(-1) # 왼쪽으로 1칸 회전: [1, 2, 3]
💡 실생활 비유: deque는 양쪽 문이 있는 지하철 같아요! 한쪽 문으로만 타고 내릴 수 있는 일반 리스트와 달리, deque는 양쪽 문에서 빠르게 타고 내릴 수 있어요!
deque가 왜 빠를까요? ⚡
- 양쪽 끝 추가/제거: O(1) (리스트는 앞쪽 O(n))
- 회전(rotation) 기능 내장
- 최근 N개 항목 유지 쉬워요!
4.2 deque의 장점
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections import deque
import time
# 리스트 vs deque 성능 비교
# 리스트: 앞쪽 삽입/삭제 느림 (O(n))
lst = []
start = time.time()
for i in range(10000):
lst.insert(0, i)
print(f"리스트: {time.time() - start:.4f}초")
# deque: 앞쪽 삽입/삭제 빠름 (O(1))
dq = deque()
start = time.time()
for i in range(10000):
dq.appendleft(i)
print(f"deque: {time.time() - start:.4f}초")
4.3 deque 실전 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from collections import deque
# 최근 N개 항목 유지
def keep_recent(n):
"""최근 N개 항목만 유지하는 버퍼"""
buffer = deque(maxlen=n)
return buffer
history = keep_recent(3)
for i in range(5):
history.append(i)
print(list(history))
# 출력:
# [0]
# [0, 1]
# [0, 1, 2]
# [1, 2, 3] # 0이 제거됨
# [2, 3, 4] # 1이 제거됨
# 슬라이딩 윈도우
def sliding_window(seq, n):
"""슬라이딩 윈도우"""
window = deque(maxlen=n)
for item in seq:
window.append(item)
if len(window) == n:
yield list(window)
for window in sliding_window([1, 2, 3, 4, 5], 3):
print(window)
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
4.4 ChainMap: 여러 딕셔너리 연결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from collections import ChainMap
# 여러 딕셔너리를 하나처럼 사용
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
dict3 = {'c': 5, 'd': 6}
chain = ChainMap(dict1, dict2, dict3)
print(chain['a']) # 1 (dict1에서)
print(chain['b']) # 2 (dict1에서, 먼저 나온 것 사용)
print(chain['c']) # 4 (dict2에서)
print(chain['d']) # 6 (dict3에서)
# 실전: 설정 우선순위
default_config = {'host': 'localhost', 'port': 8080, 'debug': False}
user_config = {'port': 3000, 'debug': True}
config = ChainMap(user_config, default_config)
print(config['host']) # localhost (default)
print(config['port']) # 3000 (user 설정 우선)
print(config['debug']) # True (user 설정 우선)
실무에서 이렇게 써요! 💼
- 설정 파일 우선순위 관리 (사용자 설정 > 기본 설정)
- 여러 딕셔너리를 하나처럼 사용
- 스코프 체인 구현 (변수 검색)
💡 핵심: ChainMap은 여러 설정을 계층적으로 관리할 때 아주 유용해요! Flask, Django 같은 프레임워크에서 설정 관리에 이런 패턴을 써요!
💡 오늘의 핵심 요약
- namedtuple - 이름으로 접근하는 튜플 📦
1 2 3
Point = namedtuple('Point', ['x', 'y']) p = Point(10, 20) print(p.x) # 10
- Counter - 자동 빈도수 계산 🔢
1
Counter(['a', 'b', 'a']).most_common(1) # [('a', 2)]
- defaultdict - KeyError 걱정 끝 🔑
1 2
dd = defaultdict(int) dd['count'] += 1 # KeyError 없음!
- deque - 양방향 빠른 큐 ⚡
1 2
dq = deque([1, 2, 3]) dq.appendleft(0) # O(1)
- ChainMap - 여러 딕셔너리 연결 🔗
1
ChainMap(user_config, default_config)
🧪 연습 문제
문제 1: 로그 분석기
로그 파일에서 각 IP 주소의 접속 횟수를 세는 프로그램을 작성하세요. namedtuple과 Counter를 사용하세요!
1
2
3
4
5
6
# 여기에 코드 작성
logs = [
# LogEntry 형식으로 로그 저장
]
# IP별 접속 횟수 출력
💡 힌트
단계별 힌트:
namedtuple로 LogEntry 정의하기 (ip, timestamp, method, path)- 로그 데이터를 LogEntry 리스트로 만들기
Counter로 IP 주소 빈도수 세기most_common()으로 정렬해서 출력
핵심 키워드: namedtuple, Counter, most_common(), 제너레이터 표현식
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import Counter, namedtuple
# 로그 항목 정의
LogEntry = namedtuple('LogEntry', ['ip', 'timestamp', 'method', 'path'])
# 로그 데이터
logs = [
LogEntry('192.168.1.1', '2025-01-01 10:00:00', 'GET', '/'),
LogEntry('192.168.1.2', '2025-01-01 10:01:00', 'POST', '/api'),
LogEntry('192.168.1.1', '2025-01-01 10:02:00', 'GET', '/about'),
LogEntry('192.168.1.3', '2025-01-01 10:03:00', 'GET', '/'),
LogEntry('192.168.1.1', '2025-01-01 10:04:00', 'DELETE', '/api/user'),
]
# IP별 접속 횟수
ip_counter = Counter(log.ip for log in logs)
print("📊 IP별 접속 횟수:")
for ip, count in ip_counter.most_common():
print(f" {ip}: {count}번")
# 메서드별 통계
method_counter = Counter(log.method for log in logs)
print("\n📊 메서드별 통계:")
for method, count in method_counter.items():
print(f" {method}: {count}번")
출력:
1
2
3
4
5
6
7
8
9
📊 IP별 접속 횟수:
192.168.1.1: 3번
192.168.1.2: 1번
192.168.1.3: 1번
📊 메서드별 통계:
GET: 3번
POST: 1번
DELETE: 1번
설명:
- namedtuple로 로그 구조화
- Counter로 빈도수 자동 계산
- most_common()으로 많은 순 정렬
문제 2: 최근 방문 기록
최근 5개의 URL만 유지하는 방문 기록 시스템을 deque로 만드세요!
💡 힌트
단계별 힌트:
deque(maxlen=5)생성하기append()로 URL 추가하기- maxlen을 넘으면 자동으로 오래된 항목 제거
핵심 키워드: deque, maxlen, append()
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
from collections import deque
# 최근 5개만 유지하는 방문 기록
history = deque(maxlen=5)
# 페이지 방문
pages = ['home', 'about', 'products', 'contact', 'blog', 'faq', 'support']
print("📜 방문 기록:")
for page in pages:
history.append(page)
print(f"방문: {page} → 기록: {list(history)}")
출력:
1
2
3
4
5
6
7
8
📜 방문 기록:
방문: home → 기록: ['home']
방문: about → 기록: ['home', 'about']
방문: products → 기록: ['home', 'about', 'products']
방문: contact → 기록: ['home', 'about', 'products', 'contact']
방문: blog → 기록: ['home', 'about', 'products', 'contact', 'blog']
방문: faq → 기록: ['about', 'products', 'contact', 'blog', 'faq']
방문: support → 기록: ['products', 'contact', 'blog', 'faq', 'support']
설명:
- maxlen=5로 최대 5개만 유지
- 6번째 추가하면 첫 번째 자동 제거
- 브라우저 히스토리 같은 기능!
📝 오늘 배운 내용 정리
- namedtuple: 구조화된 불변 데이터를 이름으로 접근해요
- Counter: 빈도수를 자동으로 계산해주는 똑똑한 도구예요
- defaultdict: KeyError 없이 기본값을 자동 생성해요
- deque: 양쪽 끝에서 빠른 추가/제거가 가능해요 (O(1))
- ChainMap: 여러 딕셔너리를 하나처럼 사용할 수 있어요
🔗 관련 자료
📚 이전 학습
Day 67: 함수형 프로그래밍 심화 ⭐⭐⭐
어제는 functools 모듈의 partial, lru_cache, reduce 등을 배웠어요!
📚 다음 학습
Day 69: 타입 힌팅과 데이터클래스 ⭐⭐⭐
내일은 타입 힌팅으로 코드를 안전하게 만들고, dataclass로 클래스를 간단하게 작성하는 방법을 배워요!
“올바른 도구를 사용하면 코딩이 훨씬 쉬워져요!” 🚀
Day 68/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
