본문으로 건너뛰기
Previous
Next
Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드

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

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

이 글의 핵심

리스트·딕셔너리·튜플·세트의 쓰임과 함께, CPython에서 list와 dict가 어떤 C 구조체에 대응하고 메모리·조회 비용이 어디서 오는지 전문가 수준으로 정리했습니다.

들어가며

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

실무 활용 사례: 데이터 분석, 웹 개발, 자동화 프로젝트에서 실제로 사용한 패턴과 코드를 바탕으로 정리했습니다. 초보자가 흔히 겪는 오류와 해결법을 포함합니다.

실무에서 느낀 Python의 매력

처음 Python을 배울 때는 “이게 정말 프로그래밍 언어인가?” 싶을 정도로 간결했습니다. C++에서 10줄로 작성하던 코드가 Python에서는 2~3줄로 끝나는 경우가 많았죠. 특히 데이터 분석 프로젝트를 진행하면서 Pandas와 NumPy의 강력함을 체감했습니다. 엑셀로 몇 시간 걸리던 작업이 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()

CPython 구현 관점: listdict의 메모리·레이아웃

이 절은 사용법이 아니라 CPython 소스 수준에서 자주 언급되는 모델을 요약합니다. 필드 이름·내부 최적화는 마이너·메이저 버전마다 바뀔 수 있으므로, 여기서는 “왜 이런 복잡도·메모리 특성이 나오는가”를 잡는 데 초점을 둡니다.

리스트(list): 가변 배열과 과할당(overallocation)

CPython의 list는 논리적으로 PyObject* 포인터들의 연속 구역을 가리키는 구조로 이해할 수 있습니다(구현 세부는 Objects/listobject.c 계열). 핵심 아이디어는 다음과 같습니다.

  • ob_size(논리 길이): len()에 대응하는 요소 개수입니다.
  • allocated(할당 용량): 실제로 예약해 둔 슬롯은 종종 논리 길이보다 큽니다. append가 자주 일어날 때 매번 realloc을 호출하지 않도록 미리 여유 공간을 잡는 전략이 있습니다.
  • 과할당: 끝에 붙이는 연산은 상각(amortized) 복잡도 O(1) 로 기대할 수 있지만, 중간 삽입·삭제는 뒤쪽 요소를 밀거나 당겨야 하므로 최악 O(n) 입니다.

메모리 레이아웃 관점에서 리스트는 “객체들을 힙 곳곳에 두되, 리스트 본체는 그 객체들을 가리키는 포인터 배열을 가진다”고 보면 됩니다. 따라서 리스트가 커질수록 포인터 배열 자체요소 객체가 각각 메모리를 차지합니다(작은 정수 등은 interning 덕에 중복이 줄 수 있으나 일반화는 금물).

딕셔너리(dict): 해시 테이블과 삽입 순서

Python 3.7+에서는 dict가 삽입 순서를 보장합니다. 이는 “연결 리스트로만 순서를 유지한다”기보다, 내부 엔트리 배열인덱스·해시 설계가 맞물린 결과로 이해하는 편이 정확합니다. CPython 3.6 이후의 컴팩트 dict(compact dict) 아이디어는, 키·값을 분리된 밀집 배열에 두고 해시 테이블 쪽은 인덱스 역할을 하도록 해 캐시 친화성메모리를 동시에 노리는 방향입니다(세부는 버전별로 진화함).

조회·삽입의 평균적인 비용은 O(1) 에 가깝지만, 다음을 기억해야 합니다.

  • 해시 충돌·탐사(probing): 같은 슬롯에 몰리면 느려질 수 있습니다.
  • 리사이즈: 테이블이 차면 더 큰 테이블로 옮기며 재배치가 발생합니다. 한 번에 많은 키를 넣을 때 간헐적인 스파이크가 찍힐 수 있습니다.
  • 키로 쓸 수 있는 타입: __hash____eq__ 규약을 만족해야 합니다. 가변 객체를 키로 쓰면 안 되는 이유도 여기서 나옵니다(해시가 변하면 슬롯을 찾을 수 없음).

dictlist를 고를 때 구현 관점 힌트

  • 인덱스로 순차 접근이 중요하고 많이 붙이는 작업이면 리스트가 자연스럽습니다.
  • 키로 빠른 조회가 핵심이면 dict가 맞습니다. 다만 매우 큰 dict는 메모리 사용량이 커질 수 있고, 작은 dict를 아주 많이 만들면 오버헤드가 누적됩니다.
  • 알고리즘 글(시간 복잡도 표)과 함께 보면, “평균 O(1)의 조건”이 무엇인지 더 잘 정렬됩니다.

자주 하는 실수와 해결법

실수 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 연산 빠름, 집합 연산 지원
  5. CPython 관점: list는 포인터 배열·과할당, dict는 해시 테이블·(3.7+) 삽입 순서·컴팩트 엔트리 설계로 평균 O(1) 조회를 노림

다음 단계


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Python 자료형 | 리스트, 딕셔너리, 튜플, 세트 완벽 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Python list·dict 사용법과 CPython 구현 관점(PyListObject·해시 테이블·메모리 레이아웃)을 함께 다룹니다. 튜플·세트와 시간 복잡도 선택 가이드까지 한글로 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. Python 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Python, 자료형, CPython, list, dict, 메모리, 해시테이블 등으로 검색하시면 이 글이 도움이 됩니다.