# Part 3: 파이썬 컬렉션 심화

**⬅️ 이전 시간: [Part 2: 파이썬 핵심 문법 마스터하기](../02_python_core_syntax/part_2_python_core_syntax.md)**
**➡️ 다음 시간: [Part 4: 객체 지향 프로그래밍 (OOP)](../04_object_oriented_programming/part_4_object_oriented_programming.md)**

---

## 1. 학습 목표 (Learning Objectives)

이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.

- 리스트의 다양한 메서드를 활용하여 데이터를 자유자재로 조작할 수 있습니다.
- `sort()`와 `sorted()`의 차이점을 명확히 구분하여 사용합니다.
- `Counter`, `defaultdict`, `deque`, `namedtuple` 등 특수 목적 컬렉션의 장점을 이해하고 적재적소에 활용할 수 있습니다.
- [튜플 언패킹](../../glossary.md#튜플-언패킹-tuple-unpacking)을 사용하여 간결하고 가독성 높은 코드를 작성합니다.
- 문제 상황에 가장 적합한 [자료구조](../../glossary.md#자료구조-data-structure)를 선택하여 코드의 효율성을 높일 수 있습니다.

## 2. 핵심 요약 (Key Summary)
이 파트에서는 파이썬의 기본 컬렉션(리스트, 튜플, 딕셔너리)을 더 깊이 있게 다룹니다. 리스트의 `sort()`와 `sorted()` 함수의 차이점을 배우고, `collections` 모듈의 특수 목적 컬렉션인 `Counter`, `defaultdict`, `deque`, `namedtuple`을 학습합니다. 이러한 자료구조들을 언제, 어떻게 사용해야 코드의 효율성과 가독성을 높일 수 있는지 이해하고, 튜플 언패킹과 같은 파이썬다운 기술을 익힙니다.

- **핵심 키워드**: `리스트 메서드(List Methods)`, `sort() vs sorted()`, `collections.Counter`, `collections.defaultdict`, `collections.deque`, `collections.namedtuple`, `튜플 언패킹(Tuple Unpacking)`

## 3. 전문가의 공구함, 컬렉션 깊이 알기 (Introduction)

지난 시간에 파이썬의 기본 뼈대를 세웠다면, 이번 주차에는 그 연장들을 넣어둘 **'전문가용 공구함(컬렉션)'**을 깊이 있게 탐구합니다. 단순히 데이터를 담는 것을 넘어, **AI 데이터 분석 및 처리**라는 특정 용도에 따라 최적화된 특수 공구들을 꺼내 쓰는 법을 배우게 됩니다. 상황에 맞는 최적의 '데이터 그릇'을 선택하는 능력을 길러, 코드의 효율성과 가독성을 한 단계 끌어올려 보겠습니다.

> [!TIP]
> 본 파트의 모든 예제 코드는 `../../source_code/part_2_3_python_syntax_collections.py` 파일에서 직접 실행해볼 수 있습니다.

---

## 4. 리스트(List) 다시 보기

> **🎯 심화 목표 1:** 리스트의 핵심 메서드를 능숙하게 사용하고, `sort()`와 `sorted()`의 차이를 명확히 이해하며, `Counter`로 데이터 빈도를 효율적으로 계산합니다.

### 4.1. `sort()` vs `sorted()`: 원본을 바꿀 것인가, 새것을 얻을 것인가?

데이터 분석 과정에서 **원본 데이터를 훼손하지 않고 보존하는 것은 매우 중요**합니다. 이 둘의 차이를 명확히 이해해야 의도치 않은 데이터 유실을 막을 수 있습니다.

- **`list.sort()` 메서드**: **원본 리스트 자체를** 정렬합니다. 반환값은 `None`입니다. (내 방 정리)
- **`sorted()` 내장 함수**: 정렬된 **새로운 리스트를 반환**합니다. 원본은 바뀌지 않습니다. (정리된 모습 촬영)

```python
my_list = [3, 1, 4, 2]
# wrong_list = my_list.sort() #! 잘못된 사용! wrong_list는 None이 됩니다.
new_list = sorted(my_list) # 올바른 사용
print(f"원본: {my_list}, 정렬된 사본: {new_list}")
```

### 4.2. 스마트한 개수 세기: `collections.Counter`
리스트나 문자열에 포함된 각 항목이 몇 번 등장하는지 세어주는 '똑똑한 재고 관리인'입니다. 특히 **자연어 처리(NLP)에서 텍스트 데이터의 단어 빈도를 계산**하는 가장 기초적이면서도 핵심적인 작업에 사용됩니다. 이를 통해 어떤 단어가 중요한지 파악하는 등 데이터 분석의 첫걸음을 뗄 수 있습니다.

```python
from collections import Counter

fruit_counts = Counter(['apple', 'banana', 'apple'])
print(f"과일 개수: {fruit_counts}")
# 출력: Counter({'apple': 2, 'banana': 1})
print(f"가장 흔한 과일: {fruit_counts.most_common(1)}")
# 출력: [('apple', 2)]
```

---

## 5. 튜플(Tuple)과 딕셔너리(Dictionary) 깊이 알기

> **🎯 심화 목표 2:** 튜플 언패킹을 자유자재로 활용하고, `defaultdict`로 딕셔너리 초기화 코드를 간소화합니다.

### 5.1. 튜플의 힘: [언패킹(Unpacking)](../../glossary.md#튜플-언패킹-tuple-unpacking)
튜플의 각 항목을 여러 변수에 한 번에 할당하는 '우아한 소포 전달' 기능입니다.

```python
# 변수 값 교환(swap)
a, b = 10, 20
a, b = b, a # a=20, b=10

# `*`를 이용한 언패킹
first, *rest, last = (1, 2, 3, 4, 5)
# first: 1, rest: [2, 3, 4], last: 5
```

### 5.2. 키가 없을 때의 구원투수: `collections.defaultdict`
존재하지 않는 키에 접근할 때 `KeyError` 대신 지정된 기본값을 자동으로 생성하는 '자동 완성 노트'입니다. `if`문을 사용해 키의 존재 여부를 매번 확인할 필요가 없어 코드가 매우 간결해집니다. **흩어져 있는 데이터를 특정 기준(e.g., 영화 장르, 사용자 그룹)에 따라 묶어서(Grouping) 분석**할 때 폭발적인 효율성을 보여줍니다.

```python
from collections import defaultdict

# 키가 없을 때 빈 리스트(list)를 기본값으로 생성
d = defaultdict(list)
d['a'].append('apple')
d['b'].append('banana')
d['a'].append('ant')
print(d) # defaultdict(<class 'list'>, {'a': ['apple', 'ant'], 'b': ['banana']})
```

---

## 6. 특수 목적 컬렉션

> **🎯 심화 목표 3:** 양방향 큐 `deque`와 이름으로 접근하는 `namedtuple`의 편리함을 학습합니다.

### 6.1. 고속 큐(Queue): `collections.deque`
데이터의 **양쪽 끝에서 항목을 추가(`append`, `appendleft`)하거나 제거(`pop`, `popleft`)하는 속도가 매우 빠른** '효율적인 대기줄'입니다. 최근 사용 목록(LRU 캐시) 등을 구현할 때 유용합니다.

```python
from collections import deque

# 최대 3개까지만 저장되는 deque
recent_plays = deque(maxlen=3)
recent_plays.append("Hype Boy")
recent_plays.append("Ditto")
recent_plays.append("OMG")
print(recent_plays) # deque(['Hype Boy', 'Ditto', 'OMG'], maxlen=3)
recent_plays.append("Super Shy") # 'Hype Boy'는 자동으로 삭제됨
print(recent_plays) # deque(['Ditto', 'OMG', 'Super Shy'], maxlen=3)
```

### 6.2. 이름을 가진 튜플: `collections.namedtuple`
튜플의 각 항목에 이름을 붙여, 인덱스(`p[0]`)뿐만 아니라 **이름(`p.x`)으로도 접근**할 수 있게 만든 '이름표가 붙은 칸막이 상자'입니다. 코드의 가독성을 획기적으로 높여줍니다.

```python
from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p1 = Point(10, 20)
print(f"좌표: ({p1.x}, {p1.y})") # 인덱스보다 훨씬 명확함
```

---

## 7. 연습 문제 (Exercises)
지금까지 배운 여러 자료구조를 활용하여 다음 문제들을 해결해보세요.

### 문제 1: 최빈 단어 분석기
**요구사항**: 주어진 영어 텍스트에서 가장 많이 등장하는 단어 3개를 찾아 출력하세요. 단, 대소문자는 구분하지 않고, 구두점은 무시합니다. (`collections.Counter` 활용)

```python
import re
from collections import Counter

text = """Python is an interpreted, high-level, general-purpose programming language."""
words = re.findall(r'\w+', text.lower())
word_counts = Counter(words)
print(f"가장 많이 나온 단어 3개: {word_counts.most_common(3)}")
```

### 문제 2: 연락처 그룹 관리
**요구사항**: 친구들의 이름을 그룹('가족', '회사' 등)별로 관리하고 출력하는 `ContactManager` 클래스를 만드세요. (`collections.defaultdict` 활용)

```python
from collections import defaultdict

class ContactManager:
    def __init__(self):
        self.contacts = defaultdict(list)
    def add_contact(self, name, group):
        self.contacts[group].append(name)
    def display_contacts(self):
        for group, names in self.contacts.items():
            print(f"- {group}: {', '.join(names)}")

# 사용 예시
manager = ContactManager()
manager.add_contact("엄마", "가족"); manager.add_contact("아빠", "가족")
manager.add_contact("김대리", "회사"); manager.add_contact("이과장", "회사")
manager.display_contacts()
```

### 문제 3: 최근 재생 목록
**요구사항**: 노래를 재생하고 최근 5곡의 재생 목록을 보여주는 `Playlist` 클래스를 만드세요. 5개를 초과하면 가장 오래된 노래가 자동으로 삭제되어야 합니다. (`collections.deque` 활용)

```python
from collections import deque

class Playlist:
    def __init__(self, max_size=5):
        self.playlist = deque(maxlen=max_size)
    def play(self, song):
        self.playlist.append(song)
    def show_recent(self):
        print(f"최근 재생 목록: {list(self.playlist)}")

# 사용 예시
playlist = Playlist(max_size=3)
playlist.play("라일락"); playlist.play("Dynamite"); playlist.play("뚜두뚜두")
playlist.play("Hype Boy") # '라일락' 삭제됨
playlist.show_recent()
```

---

## 8. 트러블슈팅 (Troubleshooting)

- **`AttributeError: 'list' object has no attribute 'appendleft'`가 발생했나요?**
  - `appendleft`는 `deque` 객체의 메서드입니다. 일반 리스트에서는 사용할 수 없습니다. `from collections import deque`를 통해 `deque`를 생성했는지 확인하세요.
- **`KeyError`가 자꾸 발생하나요?**
  - `dict`에서 존재하지 않는 키를 조회할 때 발생합니다. 키가 항상 존재한다고 보장할 수 없다면 `my_dict.get('key', 'default_value')`를 사용하거나, 데이터를 그룹핑하는 경우라면 `defaultdict`를 사용하는 것이 좋습니다.
- **어떤 자료구조를 써야 할지 모르겠나요?**
  - 아래 '컬렉션 선택 가이드' 표를 다시 한번 살펴보세요. 데이터의 특징(순서, 중복, 변경 가능성)과 내가 하려는 작업(검색, 추가/삭제)을 기준으로 가장 적합한 것을 선택하는 연습이 중요합니다.

더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.

- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**

---

## 9. 되짚어보기 (Summary)

### 컬렉션 선택 가이드: 어떤 공구를 언제 쓸까?
어떤 상황에 어떤 [자료구조](../../glossary.md#자료구조-data-structure)를 선택하는 것이 가장 효율적인지 한눈에 정리해봅시다.

| 자료구조 | 주요 특징 | 핵심 사용 사례 |
|---|---|---|
| **[`list`](../02_python_core_syntax/glossary.md#리스트-list)** | 순서 O, 변경 O | 순서가 중요하고, 데이터 추가/삭제/변경이 잦을 때 |
| **[`tuple`](../02_python_core_syntax/glossary.md#튜플-tuple)** | 순서 O, 변경 X | 내용이 바뀌면 안 되는 값의 묶음 (e.g., 좌표, DB 레코드) |
| **[`dict`](../02_python_core_syntax/glossary.md#딕셔너리-dictionary-dict)** | Key-Value, 순서 X | 의미 있는 키(Key)로 데이터를 빠르게 찾고 싶을 때 (e.g., 사용자 정보) |
| **`set`** | 순서 X, 중복 X | 중복 제거, 항목의 존재 여부 확인, 집합 연산이 필요할 때 |
| **`Counter`** | `dict`의 자식 | 항목 별 개수를 세거나, 가장 빈번한 항목을 찾을 때 (e.g., 단어 빈도수) |
| **`defaultdict`** | `dict`의 자식 | 키가 없을 때 기본값을 자동으로 생성하여 그룹핑 등을 간소화할 때 |
| **`deque`** | 양방향 큐 | 양쪽 끝에서의 추가/삭제가 빈번할 때 (e.g., 최근 목록 관리) |
| **`namedtuple`** | `tuple`의 자식 | 인덱스 대신 이름으로 데이터에 접근하여 가독성을 높이고 싶을 때 |

이러한 자료구조의 특징을 잘 이해하고 적재적소에 사용하면, 여러분의 파이썬 코드는 훨씬 더 효율적이고 읽기 좋아질 것입니다.

---

## 10. 더 깊이 알아보기 (Further Reading)

- [파이썬 공식 문서: `collections` — 컨테이너 데이터형](https://docs.python.org/ko/3/library/collections.html)
- [파이썬 공식 튜토리얼: 자료구조 심화](https://docs.python.org/ko/3/tutorial/datastructures.html)

---

## 11. 심화 프로젝트: 전자상거래 주문 데이터 분석

이번 파트에서 배운 전문가용 컬렉션들을 활용하여, 좀 더 현실적인 데이터 분석 문제를 해결해봅시다. 여러 건의 전자상거래 주문 데이터를 분석하여, 제품별 매출 현황과 인기 상품을 파악하는 스크립트를 작성하는 것이 목표입니다.

이 프로젝트를 통해 `namedtuple`로 가독성 높은 데이터 구조를 만들고, `defaultdict`와 `Counter`로 복잡한 데이터를 효율적으로 집계하는 실전 감각을 익힐 수 있습니다.

### 프로젝트 목표

주어진 주문 데이터(튜플의 리스트)를 처리하여 다음과 같은 분석 리포트를 출력하는 Python 스크립트(`order_analyzer.py`)를 완성합니다.

**샘플 주문 데이터:**
```python
# (주문 ID, 제품명, 수량, 개당 가격)
orders_data = [
    (1, "Laptop", 1, 1500000),
    (2, "Mouse", 2, 25000),
    (1, "Keyboard", 1, 45000),
    (3, "Monitor", 1, 300000),
    (2, "Keyboard", 1, 45000),
    (3, "Mouse", 1, 25000),
    (4, "Laptop", 1, 1500000),
]
```

**예상 출력 결과:**
```
E-commerce Order Analysis Report
---------------------------------
Total Revenue: 3,440,000 KRW

Product Sales Summary:
- Laptop: Total Items: 2, Total Revenue: 3,000,000 KRW
- Mouse: Total Items: 3, Total Revenue: 75,000 KRW
- Keyboard: Total Items: 2, Total Revenue: 90,000 KRW
- Monitor: Total Items: 1, Total Revenue: 300,000 KRW

Top 3 Most Sold Products:
1. Mouse (3 items)
2. Laptop (2 items)
3. Keyboard (2 items)
```

### 단계별 구현 가이드

**1. `namedtuple`로 주문 구조 정의하기**
- `collections.namedtuple`을 사용하여 'Order'라는 이름의 튜플을 만드세요.
- 필드는 `order_id`, `product_name`, `quantity`, `price_per_item` 으로 구성합니다.

**2. 데이터 분석 함수 구현하기**
- `analyze_orders(orders)` 라는 함수를 정의하고, 그 안에 다음 분석 로직을 구현하세요.

**2-1. 제품별 매출 및 판매량 집계**
- `collections.defaultdict`를 사용하여 제품별 총 매출액(`total_revenue_per_product`)을 계산하세요. 키는 제품명, 값은 총 매출액입니다.
- `collections.Counter`를 사용하여 제품별 총 판매 수량(`items_sold_per_product`)을 집계하세요.
  - (힌트) `Counter`는 `counter[key] += value` 형식으로 값을 누적할 수 있습니다.

**2-2. 전체 총 매출 계산**
- `defaultdict`에 저장된 제품별 매출액을 모두 더하여 전체 총 매출(`total_revenue`)을 계산합니다.

**2-3. 가장 많이 팔린 상품 Top 3 찾기**
- `Counter`의 `most_common(3)` 메서드를 사용하여 가장 많이 팔린 상품 3개를 찾습니다.

**3. 리포트 출력 함수 구현하기**
- `print_report(total_revenue, revenue_per_product, items_per_product)` 함수를 만들어, 위 '예상 출력 결과'와 같은 형식으로 분석 내용을 깔끔하게 출력하세요.
- 숫자는 세 자리마다 콤마(,)를 넣어주면 가독성이 더 좋습니다. (`f"{value:,}"` 포맷팅 사용)

**4. 전체 로직 통합**

아래는 위 기능들을 통합한 전체 코드의 뼈대입니다. `...` 부분을 채워 완성해보세요.

```python
from collections import namedtuple, defaultdict, Counter

# 1. namedtuple 정의
Order = namedtuple('Order', ['order_id', 'product_name', 'quantity', 'price_per_item'])

# 샘플 데이터
orders_data = [
    (1, "Laptop", 1, 1500000), (2, "Mouse", 2, 25000), (1, "Keyboard", 1, 45000),
    (3, "Monitor", 1, 300000), (2, "Keyboard", 1, 45000), (3, "Mouse", 1, 25000),
    (4, "Laptop", 1, 1500000),
]

# Order namedtuple 리스트로 변환
orders = [Order(*o) for o in orders_data]

def analyze_orders(order_list):
    # 2-1. 제품별 매출 및 판매량 집계
    revenue_per_product = defaultdict(float)
    items_per_product = Counter()
    for order in order_list:
        revenue_per_product[order.product_name] += order.quantity * order.price_per_item
        items_per_product[order.product_name] += order.quantity

    # 2-2. 전체 총 매출 계산
    total_revenue = sum(revenue_per_product.values())

    # 2-3. 가장 많이 팔린 상품 Top 3 찾기
    top_3_products = items_per_product.most_common(3)

    return total_revenue, revenue_per_product, items_per_product, top_3_products

def print_report(total_revenue, revenue_per_product, items_per_product, top_3_products):
    print("E-commerce Order Analysis Report")
    print("---------------------------------")
    print(f"Total Revenue: {total_revenue:,.0f} KRW\n")
    print("Product Sales Summary:")
    for product, total_rev in revenue_per_product.items():
        total_items = items_per_product[product]
        print(f"- {product}: Total Items: {total_items}, Total Revenue: {total_rev:,.0f} KRW")
    
    print("\nTop 3 Most Sold Products:")
    for i, (product, count) in enumerate(top_3_products, 1):
        print(f"{i}. {product} ({count} items)")

# 4. 분석 및 리포트 출력
if __name__ == "__main__":
    total_revenue, revenue_per_product, items_per_product, top_3_products = analyze_orders(orders)
    print_report(total_revenue, revenue_per_product, items_per_product, top_3_products)
```