Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드

Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드

이 글의 핵심

Python 자료형에 대한 실전 가이드입니다. 리스트, 딕셔너리, 튜플, 세트 완벽 가이드 등을 예제와 함께 상세히 설명합니다.

들어가며

Python의 내장 자료형(언어에 기본으로 들어 있는 데이터 종류)은 강력하고 사용하기 쉽습니다. 이 글에서는 리스트, 딕셔너리, 튜플, 세트를 완벽하게 마스터합니다. 자료구조 관점(시간 복잡도·알고리즘 문제 풀이)은 배열과 리스트, 스택과 큐, 해시 테이블 글과 함께 보면 이해가 깊어집니다.


1. 리스트 (List)

리스트란?

리스트(List)순서가 있고 수정 가능한(mutable) 자료형입니다. 여러 타입의 요소를 담을 수 있습니다. 쇼핑할 때 장바구니에 물건을 순서대로 넣었다가 뺄 수 있는 것과 비슷하게, 끝에 추가(append)하거나 중간을 바꾸는 일이 자유롭습니다.

특징:

  • ✅ 순서 유지 (인덱스로 접근)
  • ✅ 중복 허용
  • ✅ 수정 가능 (추가, 삭제, 변경)
  • ✅ 다양한 타입 혼합 가능

리스트 생성과 접근

# 리스트 생성
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, [1, 2]]  # 중첩 리스트 가능

# 빈 리스트
empty1 = []
empty2 = list()

# range로 리스트 생성
nums = list(range(10))
print(nums)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 인덱싱 (0부터 시작)
print(fruits[0])   # apple (첫 번째)
print(fruits[1])   # banana (두 번째)
print(fruits[-1])  # cherry (뒤에서 첫 번째)
print(fruits[-2])  # banana (뒤에서 두 번째)

# 인덱스 범위 체크
# print(fruits[10])  # IndexError: list index out of range

# 슬라이싱 [시작:끝:간격]
print(numbers[1:4])    # [2, 3, 4] (인덱스 1~3)
print(numbers[:3])     # [1, 2, 3] (처음부터 인덱스 2까지)
print(numbers[2:])     # [3, 4, 5] (인덱스 2부터 끝까지)
print(numbers[::2])    # [1, 3, 5] (간격 2)
print(numbers[::-1])   # [5, 4, 3, 2, 1] (역순)

# 슬라이싱은 새 리스트 반환 (원본 변경 안 됨)
sub = numbers[1:3]
sub[0] = 100
print(numbers)  # [1, 2, 3, 4, 5] (원본 그대로)
print(sub)      # [100, 3]

리스트 메서드 상세

fruits = ["apple", "banana"]

# 추가 메서드
fruits.append("cherry")  # 끝에 하나 추가
print(fruits)  # ['apple', 'banana', 'cherry']

fruits.insert(1, "orange")  # 인덱스 1에 삽입 (기존 요소는 뒤로)
print(fruits)  # ['apple', 'orange', 'banana', 'cherry']

fruits.extend(["grape", "kiwi"])  # 여러 개 추가 (리스트 병합)
print(fruits)  # ['apple', 'orange', 'banana', 'cherry', 'grape', 'kiwi']

# append vs extend 차이
list1 = [1, 2, 3]
list1.append([4, 5])  # 리스트 자체를 요소로 추가
print(list1)  # [1, 2, 3, [4, 5]]

list2 = [1, 2, 3]
list2.extend([4, 5])  # 요소들을 개별적으로 추가
print(list2)  # [1, 2, 3, 4, 5]

# 삭제 메서드
fruits = ["apple", "banana", "cherry", "banana"]

fruits.remove("banana")  # 첫 번째 "banana" 삭제
print(fruits)  # ['apple', 'cherry', 'banana']

del fruits[0]  # 인덱스로 삭제
print(fruits)  # ['cherry', 'banana']

last = fruits.pop()  # 마지막 요소 제거 및 반환
print(last)    # banana
print(fruits)  # ['cherry']

second = fruits.pop(0)  # 특정 인덱스 제거 및 반환
print(second)  # cherry

fruits.clear()  # 전체 삭제
print(fruits)  # []

# 검색 메서드
fruits = ["apple", "banana", "cherry", "banana"]

index = fruits.index("banana")  # 첫 번째 위치
print(index)  # 1

# index("banana", 2)  # 인덱스 2부터 검색
# fruits.index("grape")  # ValueError: 'grape' is not in list

count = fruits.count("banana")  # 개수 세기
print(count)  # 2

# in 연산자
print("apple" in fruits)  # True
print("grape" in fruits)  # False

# 정렬 메서드
numbers = [3, 1, 4, 1, 5, 9, 2]

# sort(): 원본 변경
numbers.sort()
print(numbers)  # [1, 1, 2, 3, 4, 5, 9]

numbers.sort(reverse=True)  # 내림차순
print(numbers)  # [9, 5, 4, 3, 2, 1, 1]

# sorted(): 새 리스트 반환 (원본 유지)
numbers = [3, 1, 4, 1, 5]
sorted_nums = sorted(numbers)
print(numbers)      # [3, 1, 4, 1, 5] (원본 그대로)
print(sorted_nums)  # [1, 1, 3, 4, 5]

# key 함수로 정렬
words = ["banana", "pie", "Washington", "book"]
words.sort(key=len)  # 길이 순
print(words)  # ['pie', 'book', 'banana', 'Washington']

words.sort(key=str.lower)  # 대소문자 무시
print(words)  # ['banana', 'book', 'pie', 'Washington']

# 뒤집기
numbers = [1, 2, 3, 4, 5]
numbers.reverse()  # 원본 변경
print(numbers)  # [5, 4, 3, 2, 1]

# 또는 슬라이싱
reversed_nums = numbers[::-1]  # 새 리스트

리스트 연산

# 연결 (+)
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined = list1 + list2
print(combined)  # [1, 2, 3, 4, 5, 6]

# 반복 (*)
repeated = [1, 2] * 3
print(repeated)  # [1, 2, 1, 2, 1, 2]

# 길이
print(len(fruits))  # 요소 개수

# 최대/최소/합계
numbers = [3, 1, 4, 1, 5]
print(max(numbers))  # 5
print(min(numbers))  # 1
print(sum(numbers))  # 14

리스트 컴프리헨션 (상세)

# 기본: [표현식 for 변수 in 반복가능객체]
squares = [x**2 for x in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 조건 포함: [표현식 for 변수 in 반복가능객체 if 조건]
evens = [x for x in range(10) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# if-else: [표현식1 if 조건 else 표현식2 for 변수 in 반복가능객체]
labels = ['짝수' if x % 2 == 0 else '홀수' for x in range(5)]
print(labels)  # ['짝수', '홀수', '짝수', '홀수', '짝수']

# 중첩: 2차원 리스트 생성
matrix = [[i*j for j in range(3)] for i in range(3)]
print(matrix)
# [[0, 0, 0],
#  [0, 1, 2],
#  [0, 2, 4]]

# 평탄화
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

리스트 복사 주의사항

# 얕은 복사 (shallow copy)
list1 = [1, 2, 3]
list2 = list1  # 참조 복사 (같은 객체)

list2[0] = 100
print(list1)  # [100, 2, 3] (list1도 변경됨!)

# 깊은 복사 방법
list1 = [1, 2, 3]
list2 = list1.copy()  # 방법 1
list3 = list1[:]      # 방법 2
list4 = list(list1)   # 방법 3

list2[0] = 100
print(list1)  # [1, 2, 3] (원본 유지)
print(list2)  # [100, 2, 3]

# 중첩 리스트는 얕은 복사
matrix = [[1, 2], [3, 4]]
matrix_copy = matrix.copy()
matrix_copy[0][0] = 100
print(matrix)  # [[100, 2], [3, 4]] (원본도 변경!)

# 완전한 깊은 복사
import copy
matrix_deep = copy.deepcopy(matrix)
matrix_deep[0][0] = 999
print(matrix)      # [[100, 2], [3, 4]] (원본 유지)
print(matrix_deep) # [[999, 2], [3, 4]]

2. 딕셔너리 (Dictionary)

기본 사용법

딕셔너리이름표(키)로 값을 찾는 전화번호부와 비슷합니다. 리스트처럼 순번(0, 1, 2…)으로 찾기보다 의미 있는 키로 저장하므로, 설정 값·사용자 프로필처럼 “항목 이름 → 내용” 구조에 잘 맞습니다.

# 딕셔너리 생성
person = {
    "name": "홍길동",
    "age": 25,
    "city": "서울"
}

# 접근
print(person["name"])      # 홍길동
print(person.get("age"))   # 25
print(person.get("job", "없음"))  # 기본값

# 추가/수정
person["job"] = "개발자"   # 추가
person["age"] = 26         # 수정

# 삭제
del person["city"]
job = person.pop("job")    # 제거 및 반환

딕셔너리 메서드

person = {"name": "홍길동", "age": 25}

# 키/값/아이템
keys = person.keys()      # dict_keys(['name', 'age'])
values = person.values()  # dict_values(['홍길동', 25])
items = person.items()    # dict_items([('name', '홍길동'), ('age', 25)])

# 순회 방법
# 방법 1: 키만
for key in person:
    print(key, person[key])

# 방법 2: 키-값 (권장)
for key, value in person.items():
    print(f"{key}: {value}")
# name: 홍길동
# age: 25

# update(): 딕셔너리 병합
person.update({"city": "서울", "job": "개발자"})  # age는 덮어씀
print(person)  # {'name': '홍길동', 'age': 25, 'city': '서울', 'job': '개발자'}

# setdefault(): 키가 없을 때만 추가
person.setdefault("country", "한국")  # 추가
print(person["country"])  # 한국

person.setdefault("name", "김철수")  # 이미 있으므로 무시
print(person["name"])  # 홍길동 (변경 안 됨)

# popitem(): 마지막 키-값 쌍 제거 (Python 3.7+)
last = person.popitem()
print(last)  # ('country', '한국')

딕셔너리 컴프리헨션

# 기본: {키표현식: 값표현식 for 변수 in 반복가능객체}
squares = {x: x**2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 조건 포함
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares)  # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

# 키-값 교환
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(swapped)  # {1: 'a', 2: 'b', 3: 'c'}

# 리스트를 딕셔너리로
names = ["Alice", "Bob", "Charlie"]
name_dict = {i: name for i, name in enumerate(names)}
print(name_dict)  # {0: 'Alice', 1: 'Bob', 2: 'Charlie'}

중첩 딕셔너리

# 중첩 딕셔너리
users = {
    "user1": {"name": "홍길동", "age": 25},
    "user2": {"name": "김철수", "age": 30}
}

# 접근
print(users["user1"]["name"])  # 홍길동

# 안전한 접근
name = users.get("user1", {}).get("name", "없음")
print(name)  # 홍길동

# 존재하지 않는 키
name = users.get("user3", {}).get("name", "없음")
print(name)  # 없음 (에러 없음)

defaultdict 활용

from collections import defaultdict

# 기본 딕셔너리 문제
word_count = {}
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]

for word in words:
    if word not in word_count:
        word_count[word] = 0
    word_count[word] += 1

print(word_count)  # {'apple': 3, 'banana': 2, 'cherry': 1}

# defaultdict 사용
word_count = defaultdict(int)  # 기본값 0
for word in words:
    word_count[word] += 1  # 키가 없어도 자동으로 0으로 초기화

print(dict(word_count))  # {'apple': 3, 'banana': 2, 'cherry': 1}

# 리스트를 기본값으로
groups = defaultdict(list)
students = [("홍길동", "A"), ("김철수", "B"), ("이영희", "A")]

for name, grade in students:
    groups[grade].append(name)

print(dict(groups))  # {'A': ['홍길동', '이영희'], 'B': ['김철수']}

3. 튜플 (Tuple)

튜플이란?

튜플(Tuple)순서가 있고 수정 불가능한(immutable) 자료형입니다. 리스트와 비슷하지만 한 번 생성하면 변경할 수 없습니다.

특징:

  • ✅ 순서 유지
  • ✅ 중복 허용
  • ❌ 수정 불가 (추가, 삭제, 변경 불가)
  • ✅ 리스트보다 메모리 효율적
  • ✅ 딕셔너리 키로 사용 가능

언제 사용하나?

  • 변경되면 안 되는 데이터 (좌표, 설정값)
  • 함수에서 여러 값 반환
  • 딕셔너리 키로 사용

튜플 생성과 접근

# 튜플 생성
point = (10, 20)
person = ("홍길동", 25, "서울")
single = (42,)  # 요소 1개일 때 쉼표 필수!

# 괄호 없이도 가능
coords = 10, 20, 30
print(type(coords))  # <class 'tuple'>

# 빈 튜플
empty1 = ()
empty2 = tuple()

# 리스트를 튜플로
numbers = tuple([1, 2, 3, 4, 5])
print(numbers)  # (1, 2, 3, 4, 5)

# 인덱싱/슬라이싱 (리스트와 동일)
print(person[0])    # 홍길동
print(person[-1])   # 서울
print(person[1:])   # (25, '서울')

# ❌ 수정 불가
# person[1] = 26  # TypeError: 'tuple' object does not support item assignment
# person.append(100)  # AttributeError: 'tuple' object has no attribute 'append'

# 튜플 메서드 (2개만 존재)
numbers = (1, 2, 3, 2, 4, 2)
print(numbers.count(2))  # 3 (2의 개수)
print(numbers.index(3))  # 2 (3의 인덱스)

튜플 언패킹 (Unpacking)

# 기본 언패킹
name, age, city = ("홍길동", 25, "서울")
print(name, age, city)  # 홍길동 25 서울

# 함수 반환값
def get_user():
    return "홍길동", 25, "서울"  # 튜플 반환

name, age, city = get_user()
print(name)  # 홍길동

# 값 교환 (Python의 강력한 기능)
a, b = 1, 2
a, b = b, a  # 교환
print(a, b)  # 2 1

# * 연산자로 나머지 받기
numbers = (1, 2, 3, 4, 5)
first, *rest, last = numbers
print(first)  # 1
print(rest)   # [2, 3, 4] (리스트!)
print(last)   # 5

# 여러 변수에 동시 할당
x, y, z = 10, 20, 30
print(x, y, z)  # 10 20 30

튜플 vs 리스트 비교

특징튜플리스트
수정 가능
속도빠름느림
메모리적음많음
메서드2개11개
딕셔너리 키
사용 시점불변 데이터가변 데이터
import sys

# 메모리 비교
list_data = [1, 2, 3, 4, 5]
tuple_data = (1, 2, 3, 4, 5)

print(sys.getsizeof(list_data))   # 104 bytes
print(sys.getsizeof(tuple_data))  # 80 bytes

# 딕셔너리 키로 사용
# locations = {[10, 20]: "A"}  # TypeError: unhashable type: 'list'
locations = {(10, 20): "A", (30, 40): "B"}  # 튜플은 가능
print(locations[(10, 20)])  # A

튜플의 불변성 주의사항

# 튜플 자체는 불변이지만, 내부 가변 객체는 변경 가능
tuple_with_list = (1, 2, [3, 4])
# tuple_with_list[0] = 100  # TypeError (튜플 요소 변경 불가)
tuple_with_list[2].append(5)  # 내부 리스트는 변경 가능
print(tuple_with_list)  # (1, 2, [3, 4, 5])

# 튜플 "수정" (실제로는 새 튜플 생성)
original = (1, 2, 3)
modified = original + (4, 5)  # 새 튜플
print(original)  # (1, 2, 3)
print(modified)  # (1, 2, 3, 4, 5)

4. 세트 (Set)

세트란?

세트(Set)순서가 없고 중복을 허용하지 않는 자료형입니다. 수학의 집합과 동일합니다.

특징:

  • ❌ 순서 없음 (인덱스 접근 불가)
  • ❌ 중복 불가 (자동 제거)
  • ✅ 수정 가능 (추가, 삭제)
  • ✅ 멤버십 테스트 빠름 (O(1))
  • ✅ 집합 연산 지원

언제 사용하나?

  • 중복 제거
  • 멤버십 테스트 (in 연산)
  • 집합 연산 (합집합, 교집합, 차집합)

세트 생성과 연산

# 세트 생성
fruits = {"apple", "banana", "cherry"}
numbers = {1, 2, 3, 4, 5}

# 빈 세트 (주의: {}는 딕셔너리!)
empty = set()  # 올바른 방법
# empty = {}  # 이건 딕셔너리!

# 리스트를 세트로 (중복 제거)
numbers = [1, 2, 2, 3, 3, 3, 4]
unique = set(numbers)
print(unique)  # {1, 2, 3, 4}

# 문자열을 세트로
chars = set("hello")
print(chars)  # {'h', 'e', 'l', 'o'} (중복 'l' 제거)

# 추가
fruits = {"apple", "banana"}
fruits.add("cherry")
print(fruits)  # {'apple', 'banana', 'cherry'}

fruits.add("apple")  # 중복은 무시됨
print(fruits)  # {'apple', 'banana', 'cherry'}

# 여러 개 추가
fruits.update(["orange", "grape"])
print(fruits)  # {'apple', 'banana', 'cherry', 'orange', 'grape'}

# 삭제
fruits.remove("banana")  # 없으면 KeyError
print(fruits)  # {'apple', 'cherry', 'orange', 'grape'}

fruits.discard("grape")  # 없어도 에러 안 남
fruits.discard("kiwi")   # 에러 없음

# pop(): 임의의 요소 제거 (순서 없으므로 어떤 요소인지 모름)
item = fruits.pop()
print(item)  # 임의의 과일

fruits.clear()  # 전체 삭제

집합 연산 상세

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# 합집합 (Union): 두 집합의 모든 요소
print(a | b)           # {1, 2, 3, 4, 5, 6}
print(a.union(b))      # {1, 2, 3, 4, 5, 6}

# 교집합 (Intersection): 공통 요소
print(a & b)              # {3, 4}
print(a.intersection(b))  # {3, 4}

# 차집합 (Difference): a에만 있는 요소
print(a - b)            # {1, 2}
print(a.difference(b))  # {1, 2}

# 대칭 차집합 (Symmetric Difference): 한쪽에만 있는 요소
print(a ^ b)                      # {1, 2, 5, 6}
print(a.symmetric_difference(b))  # {1, 2, 5, 6}

# 부분집합/상위집합 확인
c = {1, 2}
print(c.issubset(a))    # True (c는 a의 부분집합)
print(a.issuperset(c))  # True (a는 c의 상위집합)

# 서로소 집합 확인
d = {7, 8, 9}
print(a.isdisjoint(d))  # True (공통 요소 없음)
print(a.isdisjoint(b))  # False (3, 4가 공통)

세트 실전 활용

# 1. 중복 제거 (순서 유지 필요 없을 때)
emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]
unique_emails = list(set(emails))
print(unique_emails)  # ['[email protected]', '[email protected]', '[email protected]']

# 2. 중복 제거 (순서 유지 필요할 때)
def remove_duplicates(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]
unique = remove_duplicates(emails)
print(unique)  # ['[email protected]', '[email protected]', '[email protected]']

# 3. 빠른 멤버십 테스트
# 리스트: O(n)
large_list = list(range(100000))
# 10000 in large_list  # 느림

# 세트: O(1)
large_set = set(range(100000))
# 10000 in large_set  # 빠름

# 4. 두 리스트의 공통 요소
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common = list(set(list1) & set(list2))
print(common)  # [4, 5]

실전 예제

예제 1: 학생 성적 관리 시스템

# 학생 데이터 (리스트 + 딕셔너리)
students = [
    {"name": "홍길동", "scores": [85, 90, 78]},
    {"name": "김철수", "scores": [92, 88, 95]},
    {"name": "이영희", "scores": [78, 82, 80]}
]

# 평균 계산 및 등급 부여
for student in students:
    avg = sum(student["scores"]) / len(student["scores"])
    
    if avg >= 90:
        grade = "A"
    elif avg >= 80:
        grade = "B"
    else:
        grade = "C"
    
    print(f"{student['name']}: 평균 {avg:.1f}, 등급 {grade}")

# 출력:
# 홍길동: 평균 84.3, 등급 B
# 김철수: 평균 91.7, 등급 A
# 이영희: 평균 80.0, 등급 B

예제 2: 텍스트 분석 (딕셔너리 + 세트)

text = """
Python is powerful. Python is easy.
Python is popular. Python is versatile.
"""

# 단어 추출 (소문자 변환, 구두점 제거)
words = text.lower().replace(".", "").split()

# 단어 빈도 (딕셔너리)
word_count = {}
for word in words:
    word_count[word] = word_count.get(word, 0) + 1

# 빈도순 정렬
sorted_words = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
print("단어 빈도:")
for word, count in sorted_words[:5]:  # 상위 5개
    print(f"  {word}: {count}")

# 고유 단어 개수 (세트)
unique_words = set(words)
print(f"\n고유 단어 수: {len(unique_words)}")

# 출력:
# 단어 빈도:
#   python: 4
#   is: 4
#   powerful: 1
#   easy: 1
#   popular: 1
# 
# 고유 단어 수: 6

예제 3: 장바구니 시스템 (딕셔너리 + 리스트)

# 상품 정보
products = {
    "P001": {"name": "노트북", "price": 1200000},
    "P002": {"name": "마우스", "price": 30000},
    "P003": {"name": "키보드", "price": 80000}
}

# 장바구니 (상품ID: 수량)
cart = {}

# 상품 추가
def add_to_cart(product_id, quantity=1):
    if product_id in products:
        cart[product_id] = cart.get(product_id, 0) + quantity
        print(f"{products[product_id]['name']} {quantity}개 추가")
    else:
        print("존재하지 않는 상품입니다.")

# 총액 계산
def calculate_total():
    total = 0
    for product_id, quantity in cart.items():
        price = products[product_id]["price"]
        total += price * quantity
    return total

# 실행
add_to_cart("P001", 1)  # 노트북 1개 추가
add_to_cart("P002", 2)  # 마우스 2개 추가
add_to_cart("P003", 1)  # 키보드 1개 추가

print(f"\n총액: {calculate_total():,}원")  # 총액: 1,340,000원

예제 4: 친구 추천 시스템 (세트)

# 사용자별 친구 목록
friends = {
    "홍길동": {"김철수", "이영희", "박민수"},
    "김철수": {"홍길동", "박민수", "최지훈"},
    "이영희": {"홍길동", "최지훈", "정수진"}
}

# 공통 친구 찾기
def find_common_friends(user1, user2):
    return friends[user1] & friends[user2]

# 친구 추천 (친구의 친구 - 나와 내 친구 제외)
def recommend_friends(user):
    my_friends = friends[user]
    candidates = set()
    
    for friend in my_friends:
        candidates.update(friends[friend])
    
    # 나 자신과 이미 친구인 사람 제외
    recommendations = candidates - my_friends - {user}
    return recommendations

print("홍길동과 김철수의 공통 친구:", find_common_friends("홍길동", "김철수"))
# 홍길동과 김철수의 공통 친구: {'박민수'}

print("홍길동에게 추천할 친구:", recommend_friends("홍길동"))
# 홍길동에게 추천할 친구: {'최지훈', '정수진'}

예제 5: 데이터 변환 파이프라인

# CSV 데이터 (문자열)
csv_data = """name,age,city
홍길동,25,서울
김철수,30,부산
이영희,28,서울
박민수,35,대구"""

# 1단계: 문자열 → 리스트
lines = csv_data.strip().split("\n")
header = lines[0].split(",")
rows = [line.split(",") for line in lines[1:]]

# 2단계: 리스트 → 딕셔너리 리스트
users = []
for row in rows:
    user = {}
    for i, key in enumerate(header):
        user[key] = row[i]
    users.append(user)

print("딕셔너리 리스트:")
for user in users:
    print(user)

# 3단계: 도시별 그룹핑 (딕셔너리 + 리스트)
city_groups = {}
for user in users:
    city = user["city"]
    if city not in city_groups:
        city_groups[city] = []
    city_groups[city].append(user["name"])

print("\n도시별 사용자:")
for city, names in city_groups.items():
    print(f"{city}: {', '.join(names)}")

# 4단계: 고유 도시 (세트)
cities = {user["city"] for user in users}
print(f"\n고유 도시: {cities}")

# 출력:
# 딕셔너리 리스트:
# {'name': '홍길동', 'age': '25', 'city': '서울'}
# {'name': '김철수', 'age': '30', 'city': '부산'}
# {'name': '이영희', 'age': '28', 'city': '서울'}
# {'name': '박민수', 'age': '35', 'city': '대구'}
# 
# 도시별 사용자:
# 서울: 홍길동, 이영희
# 부산: 김철수
# 대구: 박민수
# 
# 고유 도시: {'서울', '부산', '대구'}

성능 비교와 선택 가이드

시간 복잡도 비교

연산리스트튜플딕셔너리세트
인덱스 접근O(1)O(1)--
키/값 접근--O(1)-
검색 (in)O(n)O(n)O(1)O(1)
추가 (끝)O(1)-O(1)O(1)
삽입 (중간)O(n)---
삭제O(n)-O(1)O(1)
정렬O(n log n)---

자료형 선택 가이드

# 1. 순서가 중요하고 수정이 필요한 경우 → 리스트
todo_list = ["코딩", "운동", "독서"]
todo_list.append("명상")

# 2. 순서가 중요하지만 수정이 불필요한 경우 → 튜플
coordinates = (37.5665, 126.9780)  # 서울 좌표 (변경 불가)

# 3. 키로 빠르게 접근해야 하는 경우 → 딕셔너리
user_db = {"user123": {"name": "홍길동", "email": "[email protected]"}}
user = user_db["user123"]  # O(1) 접근

# 4. 중복 제거나 집합 연산이 필요한 경우 → 세트
tags1 = {"python", "coding", "tutorial"}
tags2 = {"python", "web", "tutorial"}
common_tags = tags1 & tags2  # {'python', 'tutorial'}

# 5. 빠른 멤버십 테스트가 필요한 경우 → 세트
allowed_users = {"admin", "user1", "user2"}  # 세트 (빠름)
if username in allowed_users:  # O(1)
    grant_access()

자주 하는 실수와 해결법

실수 1: 빈 세트를 {}로 생성

# ❌ 잘못된 방법
empty = {}
print(type(empty))  # <class 'dict'> (딕셔너리!)

# ✅ 올바른 방법
empty = set()
print(type(empty))  # <class 'set'>

실수 2: 리스트 복사 시 참조 문제

# ❌ 잘못된 방법
list1 = [1, 2, 3]
list2 = list1  # 참조만 복사
list2[0] = 100
print(list1)  # [100, 2, 3] (원본도 변경!)

# ✅ 올바른 방법
list1 = [1, 2, 3]
list2 = list1.copy()  # 또는 list1[:] 또는 list(list1)
list2[0] = 100
print(list1)  # [1, 2, 3] (원본 유지)

실수 3: 딕셔너리 키 에러

# ❌ 잘못된 방법
person = {"name": "홍길동"}
# print(person["age"])  # KeyError: 'age'

# ✅ 올바른 방법 1: get() 사용
age = person.get("age", 0)  # 기본값 0
print(age)  # 0

# ✅ 올바른 방법 2: in 체크
if "age" in person:
    print(person["age"])
else:
    print("나이 정보 없음")

실수 4: 튜플 요소 1개 생성

# ❌ 잘못된 방법
single = (42)
print(type(single))  # <class 'int'> (정수!)

# ✅ 올바른 방법
single = (42,)  # 쉼표 필수
print(type(single))  # <class 'tuple'>

실수 5: 세트는 순서가 없음

# ❌ 잘못된 방법
numbers = {3, 1, 4, 1, 5}
# print(numbers[0])  # TypeError: 'set' object is not subscriptable

# ✅ 올바른 방법: 리스트로 변환 후 접근
numbers_list = list(numbers)
print(numbers_list[0])  # 1 (정렬되지 않은 순서)

# 정렬이 필요하면
sorted_numbers = sorted(numbers)
print(sorted_numbers)  # [1, 3, 4, 5]

고급 활용 패턴

패턴 1: 리스트 필터링과 변환

# 데이터
users = [
    {"name": "홍길동", "age": 25, "active": True},
    {"name": "김철수", "age": 17, "active": False},
    {"name": "이영희", "age": 30, "active": True}
]

# 활성 성인 사용자만 추출
active_adults = [
    user["name"] 
    for user in users 
    if user["age"] >= 18 and user["active"]
]
print(active_adults)  # ['홍길동', '이영희']

패턴 2: 딕셔너리 병합 (Python 3.9+)

# 방법 1: | 연산자 (Python 3.9+)
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
merged = dict1 | dict2  # dict2가 우선
print(merged)  # {'a': 1, 'b': 3, 'c': 4}

# 방법 2: update()
dict1 = {"a": 1, "b": 2}
dict1.update(dict2)
print(dict1)  # {'a': 1, 'b': 3, 'c': 4}

# 방법 3: ** 언패킹
merged = {**dict1, **dict2}
print(merged)  # {'a': 1, 'b': 3, 'c': 4}

패턴 3: 리스트를 딕셔너리로 그룹핑

from collections import defaultdict

# 데이터
transactions = [
    ("2026-03-01", "식비", 15000),
    ("2026-03-01", "교통비", 5000),
    ("2026-03-02", "식비", 20000),
    ("2026-03-02", "쇼핑", 50000),
    ("2026-03-03", "식비", 12000)
]

# 날짜별 그룹핑
by_date = defaultdict(list)
for date, category, amount in transactions:
    by_date[date].append((category, amount))

print("날짜별 지출:")
for date, items in by_date.items():
    total = sum(amount for _, amount in items)
    print(f"{date}: {total:,}원")

# 카테고리별 합계
by_category = defaultdict(int)
for _, category, amount in transactions:
    by_category[category] += amount

print("\n카테고리별 지출:")
for category, total in by_category.items():
    print(f"{category}: {total:,}원")

# 출력:
# 날짜별 지출:
# 2026-03-01: 20,000원
# 2026-03-02: 70,000원
# 2026-03-03: 12,000원
# 
# 카테고리별 지출:
# 식비: 47,000원
# 교통비: 5,000원
# 쇼핑: 50,000원

패턴 4: 세트로 권한 관리

# 역할별 권한
permissions = {
    "admin": {"read", "write", "delete", "manage_users"},
    "editor": {"read", "write"},
    "viewer": {"read"}
}

# 사용자 역할
user_roles = {
    "홍길동": ["admin"],
    "김철수": ["editor", "viewer"],
    "이영희": ["viewer"]
}

# 사용자의 모든 권한 계산
def get_user_permissions(username):
    user_perms = set()
    for role in user_roles.get(username, []):
        user_perms.update(permissions.get(role, set()))
    return user_perms

# 권한 확인
def has_permission(username, permission):
    return permission in get_user_permissions(username)

# 테스트
print("홍길동 권한:", get_user_permissions("홍길동"))
# {'read', 'write', 'delete', 'manage_users'}

print("김철수 권한:", get_user_permissions("김철수"))
# {'read', 'write'}

print("이영희가 write 가능?", has_permission("이영희", "write"))
# False

연습 문제

문제 1: 리스트 회전

리스트를 오른쪽으로 k번 회전하세요.

def rotate_list(nums, k):
    if not nums or k == 0:
        return nums
    k = k % len(nums)  # k가 리스트 길이보다 클 경우
    return nums[-k:] + nums[:-k]

# 테스트
print(rotate_list([1, 2, 3, 4, 5], 2))  # [4, 5, 1, 2, 3]
print(rotate_list([1, 2, 3, 4, 5], 7))  # [4, 5, 1, 2, 3] (7 % 5 = 2)

문제 2: 두 딕셔너리의 차이 찾기

두 딕셔너리를 비교하여 다른 키-값 쌍을 찾으세요.

def dict_diff(dict1, dict2):
    all_keys = set(dict1.keys()) | set(dict2.keys())
    diff = {}
    
    for key in all_keys:
        val1 = dict1.get(key)
        val2 = dict2.get(key)
        if val1 != val2:
            diff[key] = {"old": val1, "new": val2}
    
    return diff

# 테스트
old = {"name": "홍길동", "age": 25, "city": "서울"}
new = {"name": "홍길동", "age": 26, "city": "부산", "job": "개발자"}

print(dict_diff(old, new))
# {'age': {'old': 25, 'new': 26}, 
#  'city': {'old': '서울', 'new': '부산'}, 
#  'job': {'old': None, 'new': '개발자'}}

문제 3: 중첩 리스트 평탄화

중첩된 리스트를 1차원 리스트로 변환하세요.

def flatten(nested_list):
    result = []
    for item in nested_list:
        if isinstance(item, list):
            result.extend(flatten(item))  # 재귀
        else:
            result.append(item)
    return result

# 테스트
nested = [1, [2, 3], [4, [5, 6]], 7]
print(flatten(nested))  # [1, 2, 3, 4, 5, 6, 7]

# 컴프리헨션 버전 (1단계만)
nested = [[1, 2], [3, 4], [5, 6]]
flat = [num for sublist in nested for num in sublist]
print(flat)  # [1, 2, 3, 4, 5, 6]

정리

자료형 선택 가이드

자료형순서중복수정용도
리스트순서 있는 데이터, 수정 필요
튜플불변 데이터, 함수 반환값
딕셔너리✅ (3.7+)❌ (키)키-값 쌍, 빠른 조회
세트중복 제거, 집합 연산, 빠른 멤버십

핵심 요약

  1. 리스트: 가장 범용적, append(), extend(), sort() 활용
  2. 딕셔너리: 키로 빠른 접근, get(), items(), defaultdict 활용
  3. 튜플: 불변 데이터, 언패킹, 딕셔너리 키로 사용
  4. 세트: 중복 제거, in 연산 빠름, 집합 연산 지원

다음 단계

  • Python 함수
  • Python 컴프리헨션
  • Python 클래스

관련 글

  • Python 환경 설정 | Windows/Mac에서 Python 설치하고 시작하기