# 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)**의 개념을 이해하고, 우리만의 '설계도'를 만들어 코드를 더욱 체계적으로 관리하는 방법을 배우게 될 것입니다.