포스트

[Python 100일 챌린지] Day 68 - 고급 컬렉션 (collections 모듈)

[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 같은 프레임워크에서 설정 관리에 이런 패턴을 써요!


💡 오늘의 핵심 요약

  1. namedtuple - 이름으로 접근하는 튜플 📦
    1
    2
    3
    
    Point = namedtuple('Point', ['x', 'y'])
    p = Point(10, 20)
    print(p.x)  # 10
    
  2. Counter - 자동 빈도수 계산 🔢
    1
    
    Counter(['a', 'b', 'a']).most_common(1)  # [('a', 2)]
    
  3. defaultdict - KeyError 걱정 끝 🔑
    1
    2
    
    dd = defaultdict(int)
    dd['count'] += 1  # KeyError 없음!
    
  4. deque - 양방향 빠른 큐 ⚡
    1
    2
    
    dq = deque([1, 2, 3])
    dq.appendleft(0)  # O(1)
    
  5. ChainMap - 여러 딕셔너리 연결 🔗
    1
    
    ChainMap(user_config, default_config)
    

🧪 연습 문제

문제 1: 로그 분석기

로그 파일에서 각 IP 주소의 접속 횟수를 세는 프로그램을 작성하세요. namedtuple과 Counter를 사용하세요!

1
2
3
4
5
6
# 여기에 코드 작성
logs = [
    # LogEntry 형식으로 로그 저장
]

# IP별 접속 횟수 출력
💡 힌트

단계별 힌트:

  1. namedtuple로 LogEntry 정의하기 (ip, timestamp, method, path)
  2. 로그 데이터를 LogEntry 리스트로 만들기
  3. Counter로 IP 주소 빈도수 세기
  4. 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로 만드세요!

💡 힌트

단계별 힌트:

  1. deque(maxlen=5) 생성하기
  2. append()로 URL 추가하기
  3. 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번째 추가하면 첫 번째 자동 제거
  • 브라우저 히스토리 같은 기능!

📝 오늘 배운 내용 정리

  1. namedtuple: 구조화된 불변 데이터를 이름으로 접근해요
  2. Counter: 빈도수를 자동으로 계산해주는 똑똑한 도구예요
  3. defaultdict: KeyError 없이 기본값을 자동 생성해요
  4. deque: 양쪽 끝에서 빠른 추가/제거가 가능해요 (O(1))
  5. ChainMap: 여러 딕셔너리를 하나처럼 사용할 수 있어요

🔗 관련 자료


📚 이전 학습

Day 67: 함수형 프로그래밍 심화 ⭐⭐⭐

어제는 functools 모듈의 partial, lru_cache, reduce 등을 배웠어요!

📚 다음 학습

Day 69: 타입 힌팅과 데이터클래스 ⭐⭐⭐

내일은 타입 힌팅으로 코드를 안전하게 만들고, dataclass로 클래스를 간단하게 작성하는 방법을 배워요!


“올바른 도구를 사용하면 코딩이 훨씬 쉬워져요!” 🚀

Day 68/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.