# Part 3: 파이썬 컬렉션 심화 (3주차) **⬅️ 이전 시간: [Part 2: 파이썬 핵심 문법 마스터하기](./part_2_python_core_syntax.md)** | **➡️ 다음 시간: [Part 4: 파이썬 객체 지향 프로그래밍](./part_4_object_oriented_programming.md)** --- ## 📜 실습 코드 바로가기 - **`part_2_3_python_syntax_collections.py`**: [바로가기](./source_code/part_2_3_python_syntax_collections.py) - 본 파트의 모든 예제 코드는 위 파일에서 직접 실행하고 수정해볼 수 있습니다. 리스트, 딕셔너리, 셋 등의 다양한 활용법을 직접 확인해보세요. --- 지난 시간에는 변수, 제어문, 함수 등 파이썬의 기본적인 뼈대를 세웠습니다. 이는 마치 우리가 **목공의 기본 연장(톱, 망치, 드라이버) 사용법**을 익힌 것과 같습니다. 이번 주차에는 그 연장들을 넣어둘 **'전문가용 공구함(컬렉션)'**을 깊이 있게 탐구합니다. 단순히 물건을 담는 상자가 아니라, 용도에 따라 칸이 나뉘고, 특정 작업에 최적화된 특수 공구들을 꺼내 쓰는 법을 배우게 됩니다. 상황에 맞는 최적의 '데이터 그릇'을 선택하는 능력을 길러, 코드의 효율성과 가독성을 한 단계 끌어올려 보겠습니다. --- ## 1일차(월): 리스트(List) 심화와 Counter ### 🎯 오늘의 목표 - 리스트의 핵심 메서드를 능숙하게 사용하여 데이터를 조작합니다. - `sort()`와 `sorted()`의 차이점을 명확히 이해합니다. - `collections.Counter`로 데이터의 빈도를 효율적으로 계산합니다. --- ### 1. 리스트 핵심 메서드 정복 | 메서드 | 설명 | 예제 | |---|---|---| | [`append(x)`](./glossary.md#리스트list) | 리스트 맨 끝에 항목 `x`를 추가 | `[1, 2].append(3)` → `[1, 2, 3]` | | `insert(i, x)`| `i` 위치에 항목 `x`를 삽입 | `[1, 3].insert(1, 2)` → `[1, 2, 3]` | | `remove(x)` | 리스트에서 첫 번째로 나타나는 `x`를 삭제 | `[1, 2, 1].remove(1)` → `[2, 1]` | | `pop(i)` | `i` 위치의 항목을 제거하고 반환 (생략 시 마지막 항목) | `[1, 2, 3].pop(1)` → `2` (리스트는 `[1, 3]`) | | [`sort()`](./glossary.md#sort-vs-sorted) | **원본 리스트**를 오름차순으로 정렬 | `x=[3,1,2]; x.sort()` → `x`는 `[1,2,3]` | | `reverse()` | **원본 리스트**의 순서를 뒤집음 | `x=[1,2,3]; x.reverse()` → `x`는 `[3,2,1]` | ### 2. `.sort()` vs `sorted()`: 원본을 바꿀 것인가, 새것을 얻을 것인가? 이 둘의 차이점을 이해하는 것은 매우 중요합니다. - **`list.sort()` 메서드**: **원본 [리스트](./glossary.md#리스트list) 자체를** 정렬합니다. 반환값은 `None`입니다. - **`sorted()` 내장 함수**: 정렬된 **새로운 [리스트](./glossary.md#리스트list)를 반환**합니다. 원본 리스트는 바뀌지 않습니다. ```python # .sort() 메서드 my_list = [3, 1, 4, 2] print(f"sort() 전: {my_list}") result = my_list.sort() print(f"sort() 후: {my_list}") print(f"반환값: {result}") # None # ⚠️ 흔한 실수: sort()의 반환값을 변수에 담지 마세요! wrong_list = [3, 1, 4, 2] wrong_list = wrong_list.sort() # wrong_list.sort()는 None을 반환하므로... print(f"잘못된 사용: {wrong_list}") # None이 출력됩니다! # sorted() 함수 my_list = [3, 1, 4, 2] print(f"sorted() 전: {my_list}") new_list = sorted(my_list) print(f"sorted() 후 원본: {my_list}") # 원본은 그대로 print(f"반환된 새 리스트: {new_list}") ``` #### 💡 쉽게 이해하기: `.sort()`는 '내 방 정리', `sorted()`는 '정리된 모습 촬영' > - **`my_list.sort()`**: '내 방 서랍(원본 리스트) 속 옷을 직접 정리정돈하는 것'과 같습니다. 서랍 속의 내용물 자체가 바뀌고, 정리 행위 자체는 어떤 결과물(반환값)을 남기지 않습니다 (`None`). > - **`new_list = sorted(my_list)`**: '서랍 속 옷은 그대로 둔 채(원본 유지), 침대 위에 옷을 가지런히 배열해보고(새로운 리스트 생성) 그 모습을 사진으로 찍어(새 변수에 저장) 간직하는 것'과 같습니다. ### 3. 스마트한 개수 세기: `collections.Counter` 리스트나 문자열에 포함된 각 항목이 몇 번 등장하는지 세어야 할 때, [`collections.Counter`](./glossary.md#collectionscounter)를 사용하면 매우 간결하고 효율적입니다. ```python from collections import Counter # 리스트에서 각 과일의 개수 세기 fruit_basket = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'] fruit_counts = Counter(fruit_basket) print(fruit_counts) # 출력: Counter({'apple': 3, 'banana': 2, 'orange': 1}) # 가장 흔한 항목 2개 찾기 print(fruit_counts.most_common(2)) # 출력: [('apple', 3), ('banana', 2)] # 특정 항목의 개수 확인 (딕셔너리처럼 사용) print(f"사과는 몇 개? {fruit_counts['apple']}") # 3 print(f"포도는 몇 개? {fruit_counts['grape']}") # 키가 없어도 에러 없이 0을 반환 ``` #### 💡 쉽게 이해하기: Counter는 '똑똑한 재고 관리인' > `Counter`는 '재고 관리인'입니다. 과일 바구니(`list`)를 통째로 이 관리인에게 넘겨주면, "사과 3개, 바나나 2개, 오렌지 1개 있습니다"라고 즉시 보고해주는 것과 같습니다. 수작업으로 하나씩 셀 필요가 없어 매우 편리합니다. --- ## 2일차(화): 튜플(Tuple)과 딕셔너리(Dictionary) 심화 ### 🎯 오늘의 목표 - 튜플 언패킹(Unpacking)을 자유자재로 활용합니다. - `defaultdict`를 사용하여 딕셔너리를 초기화하는 코드를 간소화합니다. --- ### 1. 튜플의 힘: 언패킹(Unpacking) 튜플의 각 항목을 여러 변수에 한 번에 할당하는 기능입니다. 코드를 매우 간결하고 직관적으로 만들어줍니다. ```python # 기본 언패킹 name, age, city = ("John Doe", 30, "New York") print(f"이름: {name}") # 변수 값 교환(swap) a, b = 10, 20 a, b = b, a # 임시 변수 없이 값을 교환 print(f"a: {a}, b: {b}") # a: 20, b: 10 # `*`를 이용한 언패킹 numbers = (1, 2, 3, 4, 5) first, *rest, last = numbers print(f"first: {first}") # 1 print(f"rest: {rest}") # [2, 3, 4] (리스트로 반환) print(f"last: {last}") # 5 ``` #### 💡 쉽게 이해하기: 튜플 언패킹은 '우아한 소포 전달' > 함수가 `("John Doe", 30, "New York")`이라는 소포(튜플)를 반환할 때, 우리가 `name`, `age`, `city`라는 세 사람이 팔을 벌리고 서서 각자의 물건을 한 번에 받는 모습과 같습니다. 임시 변수라는 창고에 일단 내려놓고 하나씩 꺼낼 필요 없이, 각자의 위치에 맞게 내용물을 바로 전달받아 코드가 매우 깔끔해집니다. ### 2. 키가 없을 때의 구원투수: `collections.defaultdict` 일반 [딕셔너리](./glossary.md#딕셔너리dictionary)는 존재하지 않는 키에 접근하면 `KeyError`가 발생합니다. [`defaultdict`](./glossary.md#collectionsdefaultdict)는 키가 없을 경우, **지정된 기본값을 자동으로 생성**하여 이러한 오류를 방지하고 코드를 간결하게 만들어줍니다. ```python from collections import defaultdict # 일반 딕셔너리로 리스트에 항목 추가하기 regular_dict = {} words = ['apple', 'ant', 'bee', 'banana', 'cat'] for word in words: first_char = word[0] if first_char not in regular_dict: regular_dict[first_char] = [] # 키가 없을 때 빈 리스트를 직접 생성 regular_dict[first_char].append(word) print(f"일반 딕셔너리: {regular_dict}") # defaultdict 사용하기 (훨씬 간결!) # 키가 없을 때 기본값으로 빈 리스트(list)를 생성하도록 지정 default_dict = defaultdict(list) for word in words: default_dict[word[0]].append(word) # if문 없이 바로 append 가능 print(f"defaultdict 사용: {default_dict}") ``` #### 💡 쉽게 이해하기: defaultdict는 '자동 완성 노트' > 일반 딕셔너리가 '일반 노트'라면, `defaultdict`는 '자동 완성 기능이 있는 마법 노트'입니다. > > 일반 노트에서는 'a'라는 페이지가 없으면 직접 'a' 페이지를 만들고 줄을 그은 뒤(`if 'a' not in...: regular_dict['a'] = []`), 내용을 써야 합니다. > > 하지만 `defaultdict(list)`라는 마법 노트에서는, 존재하지 않는 'a' 페이지를 펼치려는 순간, 노트가 자동으로 'a'라는 제목의 새 페이지를 만들고 목록을 쓸 수 있도록 준비해줍니다. 우리는 `if`문 확인 없이 그냥 내용을 추가하기만 하면 됩니다. **언제 `defaultdict`를 사용할까?** - **데이터 그룹핑**: 학생들을 학급별로, 단어들을 첫 글자별로 묶는 등, 특정 기준에 따라 아이템을 리스트나 셋에 누적할 때 `if`문 없이 깔끔한 코드를 작성할 수 있습니다. - **빈도수 계산**: 각 항목의 출현 횟수를 셀 때, 키가 없을 때마다 0으로 초기화하는 코드를 생략할 수 있습니다. (`defaultdict(int)`) --- ## 3일차(수): 셋(Set)과 특수 목적 컬렉션 ### 🎯 오늘의 목표 - 셋(Set)을 이용한 집합 연산을 마스터합니다. - 양방향 큐 자료구조인 `deque`의 사용법을 익힙니다. - 이름으로 항목에 접근하는 `namedtuple`의 편리함을 학습합니다. --- ### 1. 셋(Set)으로 다루는 집합 연산 | 연산자 | 메서드 | 설명 | |---|---|---| | `A | B` | `A.union(B)` | **합집합**: A와 B의 모든 항목 | | `A & B` | `A.intersection(B)` | **교집합**: A와 B에 공통으로 있는 항목 | | `A - B` | `A.difference(B)` | **차집합**: A에는 있지만 B에는 없는 항목 | | `A ^ B` | `A.symmetric_difference(B)` | **대칭차집합**: A 또는 B에만 있는 항목 | ```python set_a = {1, 2, 3, 4} set_b = {3, 4, 5, 6} print(f"합집합: {set_a | set_b}") print(f"교집합: {set_a & set_b}") print(f"차집합: {set_a - set_b}") print(f"대칭차집합: {set_a ^ set_b}") ``` ### 2. 고속 큐(Queue): `collections.deque` [`deque`](./glossary.md#collectionsdeque)(데크)는 "double-ended queue"의 약자로, 리스트와 달리 **양쪽 끝에서 항목을 추가하거나 제거하는 속도가 매우 빠른** 자료구조입니다. 리스트에서 `my_list.pop(0)`나 `my_list.insert(0, ...)` 연산은 모든 항목을 한 칸씩 뒤로 밀어야 해서 매우 비효율적입니다. `deque`는 이런 단점을 완벽하게 보완합니다. ```python from collections import deque d = deque([1, 2, 3]) print(f"초기 deque: {d}") d.append(4) # 오른쪽 끝에 추가 (리스트의 append와 동일) print(f"append(4): {d}") d.appendleft(0) # 왼쪽 끝에 추가 (매우 빠름!) print(f"appendleft(0): {d}") right_val = d.pop() # 오른쪽 끝 항목 제거 print(f"pop(): {right_val}, 결과: {d}") left_val = d.popleft() # 왼쪽 끝 항목 제거 (매우 빠름!) print(f"popleft(): {left_val}, 결과: {d}") ``` #### 💡 쉽게 이해하기: deque는 '효율적인 대기줄' > - **리스트**: '한 줄로 서 있는 긴 대기줄'입니다. 맨 앞사람(`list[0]`)이 빠져나가면, 뒤에 있는 모든 사람이 한 칸씩 앞으로 움직여야 합니다. 매우 비효율적이죠. > - **`deque`**: '양쪽 끝에 문이 달린 대기 공간'입니다. 사람들이 앞문으로든 뒷문으로든 들어오고 나갈 수 있어서, 다른 사람들의 위치 이동 없이 입구와 출구에서 빠르게 작업이 이루어집니다. 데이터의 앞뒤에서 추가/삭제가 빈번할 때 `deque`를 쓰는 이유입니다. ### 3. 이름을 가진 튜플: `collections.namedtuple` 튜플의 각 항목에 이름을 붙여, 인덱스뿐만 아니라 **이름으로도 접근**할 수 있게 만든 자료구조입니다. 코드의 가독성을 획기적으로 높여줍니다. ```python from collections import namedtuple # 'Point'라는 이름의 namedtuple 클래스 생성 Point = namedtuple('Point', ['x', 'y', 'z']) p1 = Point(10, 20, 30) print(f"p1: {p1}") # 인덱스로 접근 (튜플처럼) print(f"p1[0]: {p1[0]}") # 이름으로 접근 (객체처럼!) print(f"p1.y: {p1.y}") print(f"p1.z: {p1.z}") ``` #### 💡 쉽게 이해하기: namedtuple은 '이름표가 붙은 칸막이 상자' > - **[튜플](./glossary.md#튜플tuple)**: `(10, 20, 30)`은 '내용물만 있는 투명한 칸막이 상자'입니다. 첫 번째 칸, 두 번째 칸... 하고 순서로만 내용물을 구분할 수 있습니다. > - **[`namedtuple`](./glossary.md#collectionsnamedtuple)**: `Point(x=10, y=20, z=30)`은 각 칸마다 'x좌표', 'y좌표', 'z좌표'라고 **이름표를 붙여준 상자**입니다. `p1.y`처럼 이름으로 내용물을 꺼낼 수 있으니, `p1[1]`보다 훨씬 의미가 명확하고 실수를 줄여줍니다. --- ## 4일차(목): 파이썬 자료구조 종합 실습 ### 🎯 오늘의 목표 - 지금까지 배운 여러 자료구조를 활용하여 실제 미니 문제를 해결합니다. --- ### 문제 1: 최빈 단어 분석기 **요구사항**: 주어진 영어 텍스트에서 가장 많이 등장하는 단어 3개를 찾아 출력하세요. 단, 대소문자는 구분하지 않고, 구두점은 무시합니다. (`collections.Counter` 활용) ```python import re from collections import Counter text = """ Python is an interpreted, high-level, general-purpose programming language. Python's design philosophy emphasizes code readability with its notable use of significant whitespace. Its language constructs and object-oriented approach aim to help programmers write clear, logical code for small and large-scale projects. """ # 풀이 # 1. 정규표현식으로 단어만 추출하고 소문자로 변환 words = re.findall(r'\w+', text.lower()) # 2. Counter로 단어 빈도 계산 word_counts = Counter(words) # 3. 가장 흔한 단어 3개 출력 print(f"가장 많이 나온 단어 3개: {word_counts.most_common(3)}") # 예상 출력: [('python', 3), ('code', 2), ('its', 2)] ``` ### 문제 2: 연락처 그룹 관리 **요구사항**: 친구들의 이름을 그룹('가족', '회사' 등)별로 관리하는 프로그램을 만드세요. 그룹에 친구를 추가하는 기능을 구현하세요. (`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) print(f"'{name}'님을 '{group}' 그룹에 추가했습니다.") def display_contacts(self): for group, names in self.contacts.items(): print(f"--- {group} 그룹 ---") for name in names: print(f"- {name}") # 풀이 manager = ContactManager() manager.add_contact("엄마", "가족") manager.add_contact("아빠", "가족") manager.add_contact("김대리", "회사") manager.add_contact("이과장", "회사") manager.add_contact("동생", "가족") print("\n--- 전체 연락처 ---") manager.display_contacts() ``` ### 문제 3: 최근 재생 목록 **요구사항**: 노래를 재생할 때마다 '최근 재생 목록'에 추가합니다. 이 목록은 최대 5개까지만 유지됩니다. 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) print(f"▶️ 재생: {song}") def show_recent(self): print("\n--- 최근 재생 목록 ---") for song in reversed(self.playlist): # 최근 항목부터 보여주기 print(f"- {song}") # 풀이 playlist = Playlist() playlist.play("아이유 - 라일락") playlist.play("BTS - Dynamite") playlist.play("BLACKPINK - 뚜두뚜두") playlist.play("폴킴 - 모든 날, 모든 순간") playlist.play("AKMU - 어떻게 이별까지 사랑하겠어") playlist.show_recent() # 6번째 노래 추가 -> 가장 오래된 '라일락'이 삭제됨 playlist.play("NewJeans - Hype Boy") playlist.show_recent() ``` --- ## 5일차(금): 3주차 리뷰 및 Q&A ### 🎯 오늘의 목표 - 3주차에 배운 컬렉션 심화 내용을 정리하고, 언제 어떤 자료구조를 써야 할지 감을 잡습니다. - 궁금했던 점을 질문하고 동료들과 함께 해결합니다. --- ### 컬렉션 선택 가이드: 어떤 공구를 언제 쓸까? 3주차 동안 우리는 파이썬의 강력한 컬렉션들을 배웠습니다. 이제 어떤 상황에 어떤 자료구조를 선택하는 것이 가장 효율적인지 한눈에 정리해봅시다. | 자료구조 | 주요 특징 | 핵심 사용 사례 | |---|---|---| | **`list`** | 순서 O, 변경 O | 순서가 중요하고, 데이터 추가/삭제/변경이 잦을 때 (e.g., 쇼핑 목록, 작업 큐) | | **`tuple`** | 순서 O, 변경 X | 순서가 중요하지만, 내용이 절대 바뀌면 안 될 때 (e.g., 좌표, DB 레코드, 상수 모음) | | **`dict`** | 순서 X (3.7+부터는 입력 순서 유지), Key-Value, 변경 O | 의미 있는 키(Key)로 데이터를 빠르게 찾고 싶을 때 (e.g., 사용자 정보, JSON 데이터) | | **`set`** | 순서 X, 중복 X, 변경 O | 중복을 제거하거나, 항목의 존재 여부를 빠르게 확인할 때, 집합 연산이 필요할 때 | | **`Counter`** | `dict`의 자식 | 항목 별 개수를 세거나, 가장 빈번한 항목을 찾을 때 (e.g., 단어 빈도수 분석) | | **`defaultdict`** | `dict`의 자식 | `KeyError` 없이, 키가 없을 때 기본값을 자동으로 생성하고 싶을 때 (e.g., 데이터 그룹핑) | | **`deque`** | 양방향 큐, `list`와 유사 | 데이터의 양쪽 끝에서 추가/삭제가 매우 빈번할 때 (e.g., 최근 사용 목록, 너비 우선 탐색) | | **`namedtuple`** | `tuple`의 자식 | 튜플에 이름표를 붙여 가독성을 극대화하고 싶을 때 (e.g., 간단한 데이터 객체 정의) | 이러한 자료구조의 특징을 잘 이해하고 적재적소에 사용하면, 여러분의 파이썬 코드는 훨씬 더 효율적이고 읽기 좋아질 것입니다. --- 컬렉션의 세계를 깊이 탐험한 것을 축하합니다! 이제 여러분은 데이터를 단순히 담는 것을 넘어, 특성에 맞게 분류하고 효율적으로 관리하는 능력을 갖추게 되었습니다. 다음 시간에는 지금까지 배운 모든 문법과 자료구조를 '객체'라는 하나의 단위로 묶어, 더 크고 체계적인 프로그램을 설계하는 '객체 지향 프로그래밍(OOP)'의 세계로 떠나보겠습니다. **➡️ 다음 시간: [Part 4: 객체 지향 프로그래밍 (OOP)](part_4_object_oriented_programming.md)**