Python list vs tuple vs set 완벽 비교 | 자료구조 선택 가이드
이 글의 핵심
Python list, tuple, set 비교 - 가변성, 성능, 메모리 차이와 선택 기준
들어가며
“리스트만 쓰면 되는 것 아닌가요?” Python을 처음 배울 때 자주 나오는 질문입니다. 이 글에서는 list, tuple, set의 차이를 명확히 이해하고, 상황에 맞는 자료구조를 선택하는 방법을 다룹니다.
비유로 말씀드리면, list는 순서가 있는 줄 서기, tuple은 한 번 찍은 스티커 사진(바꿀 수 없음), set은 중복 없이 모아 두는 주머니에 가깝습니다. “빠른 포함 검사”가 중요하면 set을 떠올리시면 됩니다.
언제 list를, 언제 tuple·set을 쓰나요?
| 관점 | list | tuple | set |
|---|---|---|---|
| 성능 | 끝쪽 삽입은 편함, 멤버십은 O(n) | 불변·해시 가능(요소가 해시 가능할 때) | 멤버십 평균 O(1) |
| 사용성 | 가변, 순서 있음 | 키로 쓰기 좋은 불변 | 중복 제거·집합 연산 |
| 적용 시나리오 | 시퀀스 처리 | 좌표·레코드 | 유일 값, 교집합 등 |
이 글을 읽으면
- list, tuple, set의 특성 차이를 이해하실 수 있습니다
- 각 자료구조의 시간 복잡도를 익히실 수 있습니다
- 메모리 사용량 차이를 파악하실 수 있습니다
- 실전에서 어떤 것을 써야 하는지 판단하실 수 있습니다
목차
1. 빠른 비교표
| 특성 | list | tuple | set |
|---|---|---|---|
| 가변성 | 가변 | 불변 | 가변 |
| 순서 | 유지 | 유지 | 유지 안 됨 (3.7+는 삽입 순서) |
| 중복 | 허용 | 허용 | 불허 |
| 인덱싱 | ✅ O(1) | ✅ O(1) | ❌ 불가능 |
| 검색 | O(n) | O(n) | O(1) 평균 |
| 추가 | append O(1) | ❌ 불가능 | add O(1) |
| 삭제 | remove O(n) | ❌ 불가능 | remove O(1) |
| 메모리 | 보통 | 적음 | 많음 |
| 용도 | 일반 목록 | 불변 데이터, 딕셔너리 키 | 중복 제거, 집합 연산 |
2. 가변성 차이
list: 가변
lst = [1, 2, 3]
lst.append(4) # ✅ [1, 2, 3, 4]
lst[0] = 10 # ✅ [10, 2, 3, 4]
lst.remove(2) # ✅ [10, 3, 4]
append: 끝에 O(1) 평균으로 붙입니다(재할당이 나면 순간적으로 더 드는 경우가 있습니다).- 인덱스 대입: 임의 위치의 요소를 바꿉니다.
remove(x): 값이x인 첫 번째 항목 하나를 제거합니다(없으면 예외).
tuple: 불변
tup = (1, 2, 3)
tup.append(4) # ❌ AttributeError: 'tuple' object has no attribute 'append'
tup[0] = 10 # ❌ TypeError: 'tuple' object does not support item assignment
- 한 번 만들면 요소 추가·치환이 되지 않아, 딕셔너리 키나 집합 원소로 쓰기에 안전한 경우가 많습니다(요소가 모두 해시 가능할 때).
set: 가변 (하지만 순서 없음)
s = {1, 2, 3}
s.add(4) # ✅ {1, 2, 3, 4}
s.remove(2) # ✅ {1, 3, 4}
s[0] # ❌ TypeError: 'set' object is not subscriptable
add/remove는 집합 연산에 맞춰져 있고, 인덱스 접근은 지원하지 않습니다(순서가 보장되지 않기 때문입니다).
3. 성능 비교
검색 성능
import time
# 데이터 준비
n = 100000
lst = list(range(n))
tup = tuple(range(n))
s = set(range(n))
# list 검색: O(n)
start = time.time()
for _ in range(1000):
99999 in lst
print(f"list: {time.time() - start:.3f}s") # 약 1.2s
# tuple 검색: O(n)
start = time.time()
for _ in range(1000):
99999 in tup
print(f"tuple: {time.time() - start:.3f}s") # 약 1.1s (list보다 약간 빠름)
# set 검색: O(1)
start = time.time()
for _ in range(1000):
99999 in s
print(f"set: {time.time() - start:.3f}s") # 약 0.0001s (10,000배 빠름!)
추가/삭제 성능
# list.append: O(1) 평균, O(n) 최악 (재할당)
lst = []
for i in range(100000):
lst.append(i) # 빠름
# list.insert(0, x): O(n) (모든 요소 이동)
lst.insert(0, -1) # 느림
# set.add: O(1) 평균
s = set()
for i in range(100000):
s.add(i) # 빠름
4. 메모리 사용량
메모리 비교
import sys
data = range(10000)
lst = list(data)
tup = tuple(data)
s = set(data)
print(f"list: {sys.getsizeof(lst):,} bytes") # 약 85,176 bytes
print(f"tuple: {sys.getsizeof(tup):,} bytes") # 약 80,064 bytes (5% 적음)
print(f"set: {sys.getsizeof(s):,} bytes") # 약 524,520 bytes (6배 많음)
왜 차이가 나나?
- tuple: 불변이므로 메타데이터 적음
- list: 동적 확장을 위한 여유 공간
- set: 해시 테이블 오버헤드 (빠른 검색 대가)
5. 실전 선택 가이드
선택 플로우차트
아래 다이어그램은 결정 → 분기 → 결과 순으로 읽으시면 됩니다. 순서가 필요하면 list/tuple 쪽으로, 순서 없이 유일 값만 필요하면 set으로 가는 흐름입니다.
graph TD
A[자료구조 선택] --> B{순서가 중요한가?}
B -->|Yes| C{수정이 필요한가?}
B -->|No| D[set]
C -->|Yes| E[list]
C -->|No| F{딕셔너리 키로 쓰나?}
F -->|Yes| G[tuple]
F -->|No| H{성능이 중요한가?}
H -->|Yes| G
H -->|No| E
상황별 선택
# 1. 일반 목록 → list
users = ['Alice', 'Bob', 'Charlie']
users.append('David')
# 2. 불변 데이터 → tuple
point = (10, 20) # 좌표
rgb = (255, 0, 0) # 색상
# 3. 중복 제거 → set
unique_ids = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3}
# 4. 빠른 검색 → set
allowed_users = {'alice', 'bob', 'charlie'}
if username in allowed_users: # O(1)
grant_access()
# 5. 딕셔너리 키 → tuple (불변만 가능)
cache = {
(10, 20): 'result1', # ✅ tuple
[10, 20]: 'result2', # ❌ TypeError: unhashable type: 'list'
}
# 6. 함수 반환값 (여러 값) → tuple
def get_user():
return ('Alice', 25, '[email protected]') # tuple
name, age, email = get_user() # 언패킹
6. 흔한 실수
실수 1: set에 인덱싱
s = {1, 2, 3}
print(s[0]) # ❌ TypeError: 'set' object is not subscriptable
# 해결: list로 변환
print(list(s)[0]) # ✅ 하지만 순서는 보장 안 됨
실수 2: tuple 수정 시도
tup = (1, 2, 3)
tup[0] = 10 # ❌ TypeError
# 해결: 새 tuple 생성
tup = (10,) + tup[1:] # ✅ (10, 2, 3)
실수 3: list를 딕셔너리 키로
cache = {}
key = [1, 2, 3]
cache[key] = 'value' # ❌ TypeError: unhashable type: 'list'
# 해결: tuple 사용
key = (1, 2, 3)
cache[key] = 'value' # ✅
7. 고급 활용
list comprehension
# 짝수만 필터링
even = [x for x in range(10) if x % 2 == 0]
# 중첩 리스트
matrix = [[i*j for j in range(5)] for i in range(5)]
set 연산
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b) # 합집합: {1, 2, 3, 4, 5, 6}
print(a & b) # 교집합: {3, 4}
print(a - b) # 차집합: {1, 2}
print(a ^ b) # 대칭 차집합: {1, 2, 5, 6}
Named tuple
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(p.x) # 10 (인덱스 대신 이름으로 접근)
print(p[0]) # 10 (인덱스도 가능)
마무리
Python 자료구조 선택의 핵심:
- 순서 + 수정 → list
- 순서 + 불변 → tuple
- 중복 제거 + 빠른 검색 → set
- 성능 측정 → 상황에 맞게 선택
핵심: 각 자료구조의 특성을 이해하고, 문제에 맞는 것을 선택하시면 성능이 크게 개선됩니다. 데이터가 컨베이어 위에서 순서대로 처리되어야 한다면 list/tuple, 중복 제거·합집합 같은 공정이 중요하면 set을 먼저 떠올려 보시면 됩니다.
관련 글
- Python 자료구조 완벽 가이드
- Python 성능 최적화
- Python collections 모듈
키워드
Python, list, tuple, set, 자료구조, 성능, 시간 복잡도, 메모리, 비교, 선택 가이드