튜플의 불변성 철학, 해시 가능성, 패킹·언패킹, namedtuple까지 완전히 이해한다.
🎯 이 강의에서 배우는 것
튜플(tuple)은 단순히 "수정 불가능한 리스트"가 아닙니다. 불변성(immutability)이 의도를 전달합니다. 이 데이터는 변해서는 안 된다는 의미. 그 의미가 코드를 읽는 사람에게 중요한 정보가 됩니다. 또한 불변성 덕분에 해시 가능해지고, 딕셔너리 키와 셋 원소로 사용할 수 있습니다.
📦 튜플 기본 — 불변 시퀀스
# 튜플 생성
t1 = (1, 2, 3)
t2 = 1, 2, 3 # 괄호 없이도 튜플
t3 = (42,) # 단일 원소 튜플 — 쉼표 필수!
t4 = (42) # 이건 int — 튜플이 아님!
print(type(t3)) # <class 'tuple'>
print(type(t4)) # <class 'int'>
# 빈 튜플
empty = ()
empty2 = tuple()
# 불변성: 수정 시 TypeError
t = (1, 2, 3)
try:
t[0] = 99 # TypeError: 'tuple' object does not support item assignment
except TypeError as e:
print(e)
# 하지만 내부의 가변 객체는 변경 가능!
mixed = ([1, 2], [3, 4])
mixed[0].append(5) # OK — 리스트 자체를 변경
print(mixed) # ([1, 2, 5], [3, 4])
# 튜플이 저장하는 것은 리스트의 참조(포인터) — 포인터는 변하지 않음
# 리스트의 내용은 리스트 자신이 관리하는 가변 공간
# 인덱싱, 슬라이싱 — 리스트와 동일
t = (10, 20, 30, 40, 50)
print(t[1]) # 20
print(t[1:3]) # (20, 30)
print(t[::-1]) # (50, 40, 30, 20, 10)
print(len(t)) # 5
⚡ 튜플이 리스트보다 빠른 이유
import timeit
import sys
# 메모리 사용량 비교
lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)
print(f"리스트: {sys.getsizeof(lst)} bytes") # 88 bytes
print(f"튜플: {sys.getsizeof(tup)} bytes") # 80 bytes
# 생성 속도 비교
t_list = timeit.timeit("[1, 2, 3, 4, 5]", number=10_000_000)
t_tuple = timeit.timeit("(1, 2, 3, 4, 5)", number=10_000_000)
print(f"리스트 생성: {t_list:.3f}초")
print(f"튜플 생성: {t_tuple:.3f}초")
# 튜플이 일반적으로 2-3배 빠름
# 왜 빠른가?
# 1. 상수 튜플은 컴파일 시점에 상수 풀(constant pool)에 저장
# 2. 재사용: 동일한 리터럴 튜플은 같은 객체를 재사용할 수 있음
import dis
# dis.dis("(1, 2, 3)") # LOAD_CONST (1, 2, 3) — 이미 있음!
# dis.dis("[1, 2, 3]") # BUILD_LIST — 매번 새로 만듦
# 상수 재사용 확인
a = (1, 2, 3)
b = (1, 2, 3)
print(a is b) # 종종 True (인터프리터가 재사용)
c = [1, 2, 3]
d = [1, 2, 3]
print(c is d) # False (항상 새 리스트)
🗝️ 해시 가능성 — 딕셔너리 키와 셋 원소
import hashlib
# 불변 객체는 해시 가능 → 딕셔너리 키, 셋 원소로 사용
print(hash((1, 2, 3))) # 2528502973977326415 (일정한 값)
print(hash((1, 2, 3))) # 같은 값
# 가변 객체는 해시 불가능
try:
print(hash([1, 2, 3])) # TypeError: unhashable type: 'list'
except TypeError as e:
print(e)
# 튜플을 딕셔너리 키로 사용
# 활용: 2D 좌표계 맵
grid = {}
grid[(0, 0)] = "출발"
grid[(1, 2)] = "장애물"
grid[(5, 5)] = "도착"
print(grid[(0, 0)]) # 출발
# grid[[0, 0]] = "출발" ← TypeError!
# 캐시 키로 활용
def memoize(func):
cache = {}
def wrapper(*args, **kwargs):
# args는 튜플 — 해시 가능 → 딕셔너리 키로 사용 가능
key = args + tuple(sorted(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@memoize
def expensive(x, y):
return x ** y
print(expensive(2, 10)) # 1024 (계산)
print(expensive(2, 10)) # 1024 (캐시에서)
🎁 패킹과 언패킹
# 튜플 패킹: 여러 값을 튜플로 묶기
t = 1, 2, 3 # 괄호 없이 쉼표만으로 튜플
print(type(t)) # tuple
# 튜플 언패킹: 튜플을 변수에 분배
x, y, z = t
print(x, y, z) # 1 2 3
# 개수가 맞아야 함
# a, b = (1, 2, 3) # ValueError: too many values to unpack
# 확장 언패킹 (*) — Python 3
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
*init, last = [1, 2, 3, 4, 5]
print(init) # [1, 2, 3, 4]
print(last) # 5
first, *middle, last = [1, 2, 3, 4, 5]
print(middle) # [2, 3, 4]
# 변수 교환 — 튜플 언패킹의 마법
a, b = 10, 20
a, b = b, a # 임시 변수 없이!
print(a, b) # 20 10
# 내부 원리: b, a를 평가 → (20, 10) 튜플 생성 → a, b에 언패킹
# 함수에서 여러 값 반환
def divmod_custom(a, b):
return a // b, a % b # 튜플 반환
quotient, remainder = divmod_custom(17, 5)
print(f"17 ÷ 5 = {quotient} 나머지 {remainder}")
📛 namedtuple — 이름 있는 튜플
from collections import namedtuple
from typing import NamedTuple
# 기본 namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4 (이름으로 접근)
print(p[0], p[1]) # 3 4 (인덱스로도 접근)
print(p) # Point(x=3, y=4)
print(p._asdict()) # {'x': 3, 'y': 4}
# 언패킹도 가능
x, y = p
print(x, y) # 3 4
# 불변 — 수정 불가
try:
p.x = 10 # AttributeError!
except AttributeError as e:
print(e)
# _replace: 일부만 바꾼 새 namedtuple 생성 (원본 불변 유지)
p2 = p._replace(x=10)
print(p) # Point(x=3, y=4)
print(p2) # Point(x=10, y=4)
# 타입 힌트가 있는 NamedTuple (Python 3.6+, 권장)
class ColorPoint(NamedTuple):
x: float
y: float
color: str = "red" # 기본값 가능
cp = ColorPoint(1.0, 2.0)
print(cp) # ColorPoint(x=1.0, y=2.0, color='red')
print(cp.color) # red
# 사용 사례: DB 쿼리 결과, API 응답의 고정 필드 등
User = namedtuple("User", ["id", "name", "email"])
users = [User(1, "홍길동", "hong@example.com"), User(2, "이순신", "lee@example.com")]
for user in users:
print(f"{user.name}: {user.email}")
⚠️ 자주 하는 실수
- 단일 원소 튜플에 쉼표 빠뜨림:
(1)은 정수 1입니다. 단일 원소 튜플은 반드시(1,)또는1,으로 작성하세요. - 튜플 내부 가변 객체 변경 후 해시 기대:
t = ([1,2], [3,4])는 포함된 리스트가 가변이라 해시 불가능합니다. 딕셔너리 키로 쓰려면 모든 원소가 불변이어야 합니다. - 언패킹 개수 불일치:
a, b = (1, 2, 3)은 ValueError입니다. 나머지를 받으려면a, b, *rest = ...확장 언패킹을 사용하세요.
📝 정리 및 다음 강의 예고
- 튜플은 불변 시퀀스 — 수정 의도가 없는 데이터, 딕셔너리 키, 함수 반환값에 사용.
- 리스트보다 메모리 적게 쓰고 생성이 빠름 — 상수 튜플은 컴파일 시점 최적화.
- 해시 가능 (내부도 모두 불변일 때) → 딕셔너리 키, 셋 원소로 사용 가능.
- namedtuple / NamedTuple로 이름 있는 튜플 → 인덱스 없이 .x, .y로 명확하게 접근.
다음 강의: 딕셔너리(dict) — 해시 테이블의 내부 원리, O(1) 접근이 가능한 이유, 그리고 딕셔너리의 모든 패턴을 배웁니다.
관련 주제
- 튜플 불변성
- 해시 가능성
- 패킹·언패킹
- 단일원소 튜플 쉼표
- namedtuple
- 튜플 vs 리스트 선택기준
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
📚 시리즈 전체 공유
파이썬 기초 40강 — 처음 배우는 프로그래밍
이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.
댓글
0/1000
불러오는 중...
