# Part 3: 데이터 관리를 위한 파이썬 컬렉션
---
### 💡 지난 시간 복습
[Part 2: 파이썬 핵심 문법 마스터하기](part_2_python_core_syntax.md)에서는 변수, 자료형, 제어문(if, for, while), 함수 등 파이썬 프로그래밍의 뼈대를 이루는 기본 문법을 학습했습니다. 이를 통해 코드의 기본적인 흐름을 제어하는 능력을 갖추게 되었습니다.
---
여러 개의 데이터를 효율적으로 저장하고 관리하는 파이썬의 핵심 자료구조를 '컬렉션(Collection)'이라고 부릅니다. 각 자료구조는 고유한 특징과 장점을 가지고 있으므로, 상황에 맞는 적절한 컬렉션을 선택하는 것은 코드의 성능과 가독성을 높이는 데 매우 중요합니다. 이 섹션에서는 리스트, 튜플, 딕셔너리, 셋의 차이점을 명확히 이해하고, 다양한 활용 예제를 통해 실전 감각을 익힙니다.
## 1. List (리스트)
- **특징**: **순서가 있는(ordered)**, **변경 가능한(mutable)** 데이터의 모음입니다. 파이썬에서 가장 보편적으로 사용되는 자료구조로, 다른 언어의 배열(Array)과 유사합니다.
- **사용 시점**: 데이터의 순서가 중요하고, 프로그램 실행 중에 내용의 추가, 수정, 삭제가 빈번하게 필요할 때 사용합니다.
### 1.1. 리스트 생성 및 기본 조작
```python
# 리스트 생성
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed_list = [1, "hello", 3.14, True]
# 인덱싱과 슬라이싱 (문자열과 동일)
print(f"첫 번째 과일: {fruits[0]}") # apple
print(f"마지막 과일: {fruits[-1]}") # cherry
print(f"1번부터 3번 앞까지: {numbers[1:3]}") # [2, 3]
```
### 1.2. 리스트의 주요 메서드
```python
fruits = ["apple", "banana", "cherry"]
print(f"초기 리스트: {fruits}")
# 요소 추가
fruits.append("orange") # 맨 끝에 추가
print(f"append('orange'): {fruits}")
fruits.insert(1, "blueberry") # 특정 인덱스에 추가
print(f"insert(1, 'blueberry'): {fruits}")
# 요소 제거
fruits.remove("cherry") # 값으로 제거 (첫 번째로 발견된 값만)
print(f"remove('cherry'): {fruits}")
popped_fruit = fruits.pop(2) # 인덱스로 제거하고, 제거된 값을 반환
print(f"pop(2): {fruits} (제거된 값: {popped_fruit})")
# 정렬 및 순서 뒤집기
numbers = [3, 1, 4, 1, 5, 9, 2]
numbers.sort() # 오름차순 정렬 (원본 리스트 변경)
print(f"numbers.sort(): {numbers}")
numbers.sort(reverse=True) # 내림차순 정렬
print(f"numbers.sort(reverse=True): {numbers}")
fruits.reverse() # 리스트 순서를 뒤집음 (원본 리스트 변경)
print(f"fruits.reverse(): {fruits}")
> 🚫 **Common Pitfall: 리스트를 순회하면서 아이템 제거하기**
> `for` 루프를 사용해 리스트를 순회하는 동안 해당 리스트의 아이템을 제거하면, 일부 아이템을 건너뛰는 예기치 못한 결과가 발생할 수 있습니다.
>
> ```python
> numbers = [1, 2, 3, 4, 5, 6]
>
> # 잘못된 방법: 리스트를 순회하며 직접 제거
> # 2, 4, 6을 제거하고 싶지만...
> for num in numbers:
> if num % 2 == 0:
> numbers.remove(num)
>
> print(f"잘못된 방법 후 리스트: {numbers}") # 예상 결과: [1, 3, 5], 실제 결과: [1, 3, 5, 6]
> ```
> - **왜 이런 일이?**: `remove(2)`가 실행되면 리스트는 `[1, 3, 4, 5, 6]`가 됩니다. 리스트의 길이가 줄어들고 인덱스가 앞으로 당겨지면서, 다음 순회 차례였던 `3`을 건너뛰고 바로 `4`로 넘어가기 때문입니다.
> - **올바른 해결책**:
> 1. **새로운 리스트 생성 (권장)**: 컴프리헨션 등을 이용해 조건을 만족하는 아이템만으로 새 리스트를 만드는 것이 가장 안전하고 파이썬다운 방법입니다.
> ```python
> numbers_original = [1, 2, 3, 4, 5, 6]
> odd_numbers = [num for num in numbers_original if num % 2 != 0]
> print(f"컴프리헨션 사용: {odd_numbers}")
> ```
> 2. **리스트의 복사본으로 순회**: 원본 리스트를 수정해야만 한다면, `numbers[:]`와 같이 리스트의 복사본을 만들어 순회하고 원본을 변경해야 합니다.
> ```python
> numbers_original = [1, 2, 3, 4, 5, 6]
> for num in numbers_original[:]: # 복사본으로 순회
> if num % 2 == 0:
> numbers_original.remove(num) # 원본에서 제거
> print(f"복사본 순회 사용: {numbers_original}")
> ```
### 1.3. 리스트 컴프리헨션 (List Comprehension)
`for` 반복문과 `if` 조건문을 한 줄로 간결하게 표현하여 새로운 리스트를 만드는 강력한 기능입니다.
```python
# 0부터 9까지의 숫자를 제곱한 리스트 생성
# 일반적인 방법
squares = []
for i in range(10):
squares.append(i**2)
print(f"일반적인 방법: {squares}")
# 리스트 컴프리헨션 사용
squares_comp = [i**2 for i in range(10)]
print(f"컴프리헨션 사용: {squares_comp}")
# 조건문을 포함한 리스트 컴프리헨션
# 1부터 10까지의 숫자 중 짝수만 제곱하여 리스트로 만들기
even_squares = [i**2 for i in range(1, 11) if i % 2 == 0]
print(f"짝수 제곱 리스트: {even_squares}")
# {1: 1, 2: 4, 3: 9, 4: 16} 형태의 딕셔너리 생성
square_dict = {num: num**2 for num in numbers}
print(f"제곱 딕셔너리: {square_dict}")
> 💡 **Tip: 아이템 개수 세기, `collections.Counter`로 스마트하게!**
> 리스트나 문자열에 포함된 각 아이템이 몇 번 등장하는지 세어야 할 때, `for` 문과 딕셔너리로 직접 구현할 수도 있지만, `collections.Counter`를 사용하면 훨씬 간결하고 효율적입니다.
>
> ```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`는 딕셔너리를 상속받아 만들어져, 딕셔너리의 모든 기능을 포함하면서도 아이템 개수를 세는 데 유용한 추가 메서드(`most_common` 등)를 제공합니다. 데이터 분석이나 자연어 처리에서 단어 빈도를 계산할 때 매우 유용하게 사용됩니다.
```
---
## 2. Tuple (튜플)
- **특징**: **순서가 있지만**, 한번 생성되면 내용을 **변경할 수 없는(immutable)** 데이터의 모음입니다. 소괄호 `()`를 사용하여 정의합니다.
- **사용 시점**: 프로그램 실행 중 절대 변하면 안 되는 값(예: 설정값, 좌표, 함수의 고정된 반환값)을 저장하거나, 딕셔너리의 키로 사용해야 할 때 유용합니다. 리스트보다 메모리를 적게 사용하고 속도가 조금 더 빠릅니다.
```python
# 튜플 생성
point = (10, 20)
colors = ("red", "green", "blue")
print(f"x좌표: {point[0]}")
# 튜플은 변경 불가능
# point[0] = 15 # 이 코드는 TypeError를 발생시킵니다.
# 튜플 패킹(packing)과 언패킹(unpacking)
person_info = ("John Doe", 30, "New York") # 패킹
name, age, city = person_info # 언패킹
print(f"이름: {name}, 나이: {age}, 도시: {city}")
# 함수에서 여러 값을 반환할 때 튜플이 유용하게 사용됨
def get_user_info(user_id):
# (사용자 이름, 이메일, 권한 등급)을 DB에서 조회했다고 가정
return ("Alice", "alice@example.com", "admin")
user_name, user_email, user_role = get_user_info(123)
print(f"사용자: {user_name}, 이메일: {user_email}, 역할: {user_role}")
```
---
## 3. Dictionary (딕셔너리)
- **특징**: '키(Key)'와 '값(Value)'을 쌍으로 저장하는, **순서가 없는(Python 3.7+ 부터는 입력 순서 유지)** 데이터 모음입니다. 키는 고유해야 하며, 일반적으로 문자열이나 숫자를 사용합니다. 중괄호 `{}`를 사용하여 정의합니다.
- **사용 시점**: 각 데이터에 고유한 이름표(Key)를 붙여 의미를 명확히 하고, 키를 통해 값을 매우 빠르게 조회하고 싶을 때 사용합니다. (JSON 형식과 매우 유사)
### 3.1. 딕셔너리 생성 및 기본 조작
```python
# 딕셔너리 생성
person = {"name": "John", "age": 25, "city": "New York"}
# 값 조회
print(f"이름: {person['name']}")
# print(person['job']) # 키가 없으면 KeyError 발생
# .get() 메서드를 사용한 안전한 조회
print(f"직업: {person.get('job')}") # 키가 없으면 None 반환
print(f"직업: {person.get('job', 'Unemployed')}") # 기본값 지정 가능
# 값 추가 및 수정
person["job"] = "Developer" # 새로운 키-값 쌍 추가
person["age"] = 26 # 기존 키의 값 수정
print(f"수정된 정보: {person}")
# 값 삭제
del person["city"]
print(f"삭제 후 정보: {person}")
```
### 3.2. 딕셔너리 순회
```python
person = {"name": "Alice", "age": 30, "city": "Seoul"}
# 1. 키 순회
for key in person.keys():
print(f"키: {key}")
# 2. 값 순회
for value in person.values():
print(f"값: {value}")
# 3. 키와 값 동시 순회 (가장 많이 사용)
for key, value in person.items():
print(f"{key}: {value}")
```
### 3.3. 딕셔너리 컴프리헨션
리스트 컴프리헨션과 유사하게, 한 줄로 간결하게 딕셔너리를 생성할 수 있습니다.
```python
numbers = [1, 2, 3, 4]
# {1: 1, 2: 4, 3: 9, 4: 16} 형태의 딕셔너리 생성
square_dict = {num: num**2 for num in numbers}
print(f"제곱 딕셔너리: {square_dict}")
```
---
## 4. Set (셋)
- **특징**: **중복을 허용하지 않는**, **순서 없는(unordered)** 데이터의 모음입니다. 중괄호 `{}`를 사용하지만, `{}`는 빈 딕셔너리를 의미하므로 빈 셋은 `set()`으로 만들어야 합니다.
- **사용 시점**: 리스트 등에서 중복된 값을 효율적으로 제거하거나, 두 데이터 집합 간의 교집합, 합집합, 차집합 등 수학적인 집합 연산이 필요할 때 효과적입니다. 특정 요소의 존재 여부를 매우 빠르게 확인해야 할 때도 사용됩니다.
```python
# 셋 생성
numbers = [1, 2, 3, 2, 1, 4, 5, 4]
unique_numbers = set(numbers)
print(f"리스트: {numbers}")
print(f"셋 (중복 제거): {unique_numbers}") # {1, 2, 3, 4, 5} (순서는 보장되지 않음)
# 요소 추가 및 제거
unique_numbers.add(6)
print(f"add(6): {unique_numbers}")
unique_numbers.remove(3)
print(f"remove(3): {unique_numbers}")
# 요소 존재 여부 확인 (리스트보다 훨씬 빠름)
print(f"5가 셋에 포함되어 있는가? {5 in unique_numbers}") # True
```
### 4.1. 집합 연산
```python
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
# 합집합 (Union)
print(f"합집합: {set_a | set_b}")
print(f"합집합 (메서드): {set_a.union(set_b)}")
# 교집합 (Intersection)
print(f"교집합: {set_a & set_b}")
print(f"교집합 (메서드): {set_a.intersection(set_b)}")
# 차집합 (Difference)
print(f"차집합 (A-B): {set_a - set_b}")
print(f"차집합 (메서드): {set_a.difference(set_b)}")
# 대칭 차집합 (Symmetric Difference) - 합집합에서 교집합을 뺀 부분
print(f"대칭 차집합: {set_a ^ set_b}")
print(f"대칭 차집합 (메서드): {set_a.symmetric_difference(set_b)}")
```
### 4.2. 컬렉션 요약 및 선택 가이드
지금까지 배운 네 가지 기본 컬렉션은 각각의 고유한 특징을 가지고 있어, 상황에 맞게 선택하는 것이 중요합니다. 아래 다이어그램은 어떤 상황에서 어떤 자료구조를 선택해야 하는지에 대한 간단한 가이드입니다.
```mermaid
graph TD
subgraph "자료구조 선택 가이드"
direction LR
start("데이터가 여러 개인가?") -->|"Yes"| q1("순서가 중요한가?")
start -->|"No"| others("단일 변수 사용")
q1 -->|"Yes"| q2("데이터 변경이 필요한가?")
q1 -->|"No"| q3("Key-Value 형태인가?")
q2 -->|"Yes"| list_("[List]
- 순서 O
- 변경 O
- 중복 O
- 사용 예: 할 일 목록, 학생 명단")
q2 -->|"No"| tuple_("[Tuple]
- 순서 O
- 변경 X
- 중복 O
- 사용 예: 함수의 반환값, 좌표")
q3 -->|"Yes"| dict_("[Dictionary]
- Key-Value
- Key 중복 X
- 순서 O (3.7+)
- 사용 예: 사람의 정보, JSON 데이터")
q3 -->|"No"| set_("[Set]
- 순서 X
- 중복 X
- 집합 연산
- 사용 예: 중복 아이템 제거, 멤버십 테스트")
end
```
이 가이드를 통해 해결하려는 문제의 성격에 가장 적합한 자료구조를 선택하여 더 효율적이고 읽기 좋은 코드를 작성할 수 있습니다.
---
## 5. 종합 연습문제
아래 문제들을 풀어보면서 리스트, 튜플, 딕셔너리, 셋의 활용법을 익혀보세요.
### 문제 1: 리스트 컴프리헨션 활용
`names` 리스트에서 5글자 이상인 이름만 대문자로 변경하여 새로운 리스트 `long_names_upper`를 만드세요.
```python
names = ["Alice", "Bob", "Charlie", "David", "Eva", "Frank"]
# 여기에 코드를 작성하세요
long_names_upper = [name.upper() for name in names if len(name) >= 5]
print(long_names_upper)
# 예상 출력: ['CHARLIE', 'DAVID', 'FRANK']
```
### 문제 2: 튜플 리스트 데이터 처리
학생들의 정보가 `(이름, 점수)` 형태의 튜플로 리스트에 저장되어 있습니다. 점수가 80점 이상인 학생들의 이름만 추출하여 `high_scorers` 리스트를 만드세요.
```python
students = [("Alice", 85), ("Bob", 72), ("Charlie", 95), ("David", 68), ("Eva", 88)]
# 여기에 코드를 작성하세요
high_scorers = [name for name, score in students if score >= 80]
print(high_scorers)
# 예상 출력: ['Alice', 'Charlie', 'Eva']
```
### 문제 3: 딕셔너리를 이용한 데이터 집계
여러 과목의 성적이 딕셔너리로 주어졌을 때, 각 학생의 평균 점수를 계산하여 `average_scores` 딕셔너리에 저장하세요.
```python
scores = {
"Alice": {"math": 90, "english": 85, "science": 92},
"Bob": {"math": 78, "english": 80, "science": 75},
"Charlie": {"math": 88, "english": 92, "science": 95}
}
average_scores = {}
# 여기에 코드를 작성하세요
for name, subject_scores in scores.items():
average_scores[name] = sum(subject_scores.values()) / len(subject_scores)
print(average_scores)
# 예상 출력: {'Alice': 89.0, 'Bob': 77.666..., 'Charlie': 91.666...}
```
### 문제 4: 셋을 이용한 공통 항목 찾기
두 개의 프로젝트에 참여하고 있는 팀원들의 명단이 각각 리스트로 주어졌습니다. 두 프로젝트에 모두 참여하고 있는 팀원을 찾아 `common_members` 셋으로 만드세요.
```python
project_a_members = ["Alice", "Bob", "Charlie", "David"]
project_b_members = ["Charlie", "Eva", "Frank", "Alice"]
# 여기에 코드를 작성하세요
common_members = set(project_a_members) & set(project_b_members)
print(common_members)
# 예상 출력: {'Alice', 'Charlie'} (순서는 다를 수 있음)
```
---
### ✅ 정리 및 다음 단계
이번 파트에서는 파이썬에서 여러 데이터를 관리하는 핵심 도구인 **컬렉션(Collection)**에 대해 배웠습니다.
- **리스트(List)**: 순서가 있고 변경 가능한 데이터 모음
- **튜플(Tuple)**: 순서가 있지만 변경 불가능한 데이터 모음
- **딕셔너리(Dictionary)**: Key-Value 쌍으로 이루어진 데이터 모음
- **셋(Set)**: 중복을 허용하지 않는 데이터 모음
각 컬렉션의 특징을 이해하고 상황에 맞게 사용하는 것은 효율적인 코드를 작성하는 데 매우 중요합니다.
지금까지 우리는 파이썬의 기본 도구들을 모두 익혔습니다. 이제 이 도구들을 활용하여 더 체계적이고 구조적인 코드를 작성하는 방법을 배울 차례입니다.
**➡️ 다음 시간: [Part 4: 객체 지향 프로그래밍 (OOP)의 이해](part_4_object_oriented_programming.md)**
다음 시간에는 AI 라이브러리의 근간을 이루는 **객체 지향 프로그래밍(OOP)**의 세계로 들어갑니다. **클래스(Class)**와 **객체(Object)**의 개념을 이해하고, 우리만의 '설계도'를 만들어 코드를 더욱 체계적으로 관리하는 방법을 배우게 될 것입니다.