모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

27강 / 전체 32강

튜플(tuple) — 불변성의 의미와 사용 철학

9분 읽기 조회 12

튜플의 불변성 철학, 해시 가능성, 패킹·언패킹, 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

불러오는 중...