본문으로 건너뛰기
김신건의 로그

[Python] tuple: 불변 시퀀스와 NamedTuple

· 수정 · 📖 약 2분 · 761자/단어 #python #tuple #sequence #immutable #basics #namedtuple #cpython-internals
python tuple, 파이썬 튜플, namedtuple, tuple unpacking, PyTupleObject, tuple packing

정의

tuple불변(immutable) 시퀀스다. list와 거의 같은 API를 가지나 생성 후 변경 불가하며, 그 덕에 dict 키나 set 원소로 사용 가능하다(해시 가능). 함수의 다중 반환값, 좌표/레코드 같은 고정 구조에 흔히 쓰인다.

CPython 내부 구조: PyTupleObject

flowchart LR
    A["PyTupleObject"] --> B["ob_refcnt"]
    A --> C["ob_type\n(tuple_type)"]
    A --> D["ob_size (n)"]
    A --> E["ob_item[0]"]
    A --> F["ob_item[1]"]
    A --> G["..."]
    E -.->|"포인터"| H["PyObject A"]
    F -.->|"포인터"| I["PyObject B"]
  • ob_item[]: PyObject* 포인터 배열. 원소 자체가 아닌 참조를 저장
  • 불변이라 한 번 생성된 배열은 교체 불가
  • CPython 튜플 캐시: 빈 () 은 싱글턴. 길이 0~20의 튜플은 free list로 재활용
import sys

# 빈 튜플은 싱글턴
a = ()
b = ()
print(a is b)   # True

# 메모리 비교: tuple < list
lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)
print(sys.getsizeof(lst))   # 104 bytes (over-allocation)
print(sys.getsizeof(tup))   # 80 bytes (딱 필요한 만큼)

생성

empty = ()
single = (1,)        # 콤마 필수! (1)는 그냥 괄호 묶인 표현식
pair = (1, 2)
triple = 1, 2, 3     # 괄호 없이도 됨 (tuple packing)
from_iter = tuple([1, 2, 3])

함정: (1)int, (1,)tuple.

python
print(type((1)))
print(type((1,)))
print(type(()))
결과
<class 'int'>
<class 'tuple'>
<class 'tuple'>

불변성

t = (1, 2, 3)
t[0] = 99   # TypeError: 'tuple' object does not support item assignment

하지만 내부에 가변 객체가 있으면 그 객체는 변경 가능하다.

t = ([1, 2], [3, 4])
t[0].append(99)        # 리스트 자체는 변경 가능
print(t)               # ([1, 2, 99], [3, 4])

따라서 “tuple이 hashable이다”는 항상 참이 아니다. 내부에 list가 들어 있으면 hash(t)TypeError.

hash((1, 2))           # OK
hash((1, [2, 3]))      # TypeError: unhashable type: 'list'

언패킹 (Tuple Unpacking)

가장 강력하고 흔히 쓰이는 기능.

python
a, b = 1, 2
print(a, b)

# 변수 교환 (임시 변수 불필요)
a, b = b, a
print(a, b)

# 다중 반환값
def minmax(xs):
  return min(xs), max(xs)

lo, hi = minmax([3, 1, 4, 1, 5])
print(lo, hi)

# 스타 언패킹
first, *rest = (1, 2, 3, 4)
print(first, rest)

# 중간 스타
head, *middle, tail = range(5)
print(head, middle, tail)
결과
1 2
2 1
1 5
1 [2, 3, 4]
0 [1, 2, 3] 4

중첩 언패킹

# 행렬 처리
matrix = [(1, 2), (3, 4), (5, 6)]
for x, y in matrix:
    print(f"({x}, {y})")

# 깊은 중첩
((a, b), c) = ((1, 2), 3)
print(a, b, c)   # 1 2 3

# enumerate와 언패킹
for idx, (key, val) in enumerate({"a": 1, "b": 2}.items()):
    print(idx, key, val)

메서드 (불변이라 단 2개)

t = (1, 2, 3, 2, 1)
t.count(2)     # 2
t.index(3)     # 2 (처음 등장 인덱스)

읽기 연산(len, in, 슬라이스, min/max/sum)은 list와 동일.

NamedTuple: 이름 있는 필드

collections.namedtuple 또는 typing.NamedTuple로 필드명을 부여한다. 작은 데이터 클래스가 필요할 때 dataclass보다 가볍다.

python
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p)
print(p.x, p.y)
print(p[0], p[1])    # 인덱스 접근도 가능
print(p._asdict())   # dict 변환

# 기존 값으로 새 인스턴스 (불변)
q = p._replace(x=99)
print(q)
결과
Point(x=3, y=4)
3 4
3 4
{'x': 3, 'y': 4}
Point(x=99, y=4)

타입 힌트가 필요하면 typing.NamedTuple:

from typing import NamedTuple

class User(NamedTuple):
    id: int
    name: str
    age: int = 0    # 기본값

u = User(1, "Alice")
print(u.id, u.name)    # 1 Alice
print(u._fields)       # ('id', 'name', 'age')

typing.NamedTuplecollections.namedtuple을 클래스 문법으로 감싼 것. 내부는 동일한 tuple 서브클래스.

TypedDict: dict에 타입 구조 부여

typing.TypedDict는 특정 키/타입 구조를 가진 dict를 타입 힌트 수준에서 검사한다. 런타임에는 일반 dict.

from typing import TypedDict, Required, NotRequired

class UserInfo(TypedDict):
    id: int
    name: str
    email: NotRequired[str]   # 선택 필드 (3.11+)

# 사용
user: UserInfo = {"id": 1, "name": "Alice"}  # OK
user2: UserInfo = {"id": 2}   # mypy 오류: 'name' 필드 누락

tuple vs list vs dataclass vs NamedTuple vs TypedDict

타입가변해시필드명타입 힌트메모리용도
tupleXOX-최소고정 레코드, 다중 반환
listOXX-보통동적 컬렉션
namedtupleXOO제한적최소+가벼운 불변 레코드
NamedTupleXOOO최소+타입 힌트 불변 레코드
dataclassOXOO보통일반 데이터 클래스
dataclass(frozen=True)XOOO보통메서드/검증이 필요한 불변
TypedDictOXOO (정적만)dict 동일JSON-like dict 타입 표기

자세히: py-dataclass, py-typeddict-protocol

hashable 활용

# 튜플을 set 원소 / dict 키로
seen = set()
for x, y in points:
    if (x, y) in seen:
        continue
    seen.add((x, y))

# 2D 좌표 grid
grid = {(0, 0): "start", (1, 1): "wall"}
print(grid.get((0, 0)))   # start

# functools.lru_cache: 튜플 인수는 바로 캐시 키
from functools import lru_cache

@lru_cache(maxsize=None)
def shortest_path(start: tuple, end: tuple) -> int:
    ...

함수 리턴값 관용구

Python 함수는 사실상 항상 튜플을 반환한다(다중 값 = 튜플).

def stats(xs):
    return min(xs), max(xs), sum(xs) / len(xs)

lo, hi, avg = stats([1, 2, 3, 4, 5])
result = stats([1, 2, 3])   # (1, 3, 2.0) 튜플

메모리 최적화

import dis

# 상수 튜플: 컴파일 타임에 LOAD_CONST 한 번
dis.dis("x = (1, 2, 3)")    # LOAD_CONST  (1, 2, 3)

# 리스트: 매번 BUILD_LIST (3개 로드 + 빌드)
dis.dis("x = [1, 2, 3]")    # BUILD_LIST

__slots__와 tuple 조합

class Point:
    __slots__ = ("x", "y")   # dict 없이 고정 슬롯

    def __init__(self, x, y):
        self.x = x
        self.y = y

# vs NamedTuple: 둘 다 메모리 효율적이나 NamedTuple은 불변 + 시퀀스 API

대용량 데이터 처리

# list보다 tuple 순회가 약간 빠름 (size 고정, over-allocation 없음)
import timeit
n = 1_000_000
xs_list = list(range(n))
xs_tuple = tuple(range(n))

# 순회 속도 거의 동일 (접근 패턴이 동일)
# 생성/메모리 차이가 주요 이점

함정

단일 원소 튜플의 콤마

t = (42)    # int, 튜플 아님!
t = (42,)   # tuple (1,)
t = 42,     # tuple, 괄호 없어도 됨

WARNING

(42)int(42)와 동일. 단일 원소 튜플은 반드시 뒤에 쉼표 필요. (42,) 또는 42,.

내부 가변 객체와 해시

t = (1, [2, 3])
hash(t)   # TypeError: unhashable type: 'list'

# dict 키나 set 원소로 사용 불가
d = {t: "value"}   # TypeError

WARNING

내부 리스트가 있으면 tuple도 unhashable. dict 키/set 원소로 쓰려면 내부 원소까지 모두 hashable이어야 한다.

_replace()는 새 객체 반환

p = Point(x=1, y=2)
p._replace(x=99)   # 새 Point(x=99, y=2) 반환
print(p)           # Point(x=1, y=2) - 원본 변경 없음

관련 위키

이 글의 용어 (9개)
[Python] 가비지 컬렉션: refcount + 순환 GCpython
정의 CPython의 메모리 관리는 두 메커니즘의 조합이다. 1. Reference counting: 모든 객체에 참조 카운트( )가 있고, 카운트가 0이 되면 즉시 해제. 즉각…
[Python] collections: Counter, deque, defaultdict, OrderedDict, ChainMappython
정의 모듈은 dict/list/tuple/set의 특수화 컨테이너를 제공한다. 표준 라이브러리에 있는 데이터 구조 도구함의 핵심. | 클래스 | 용도 | |--------|---…
[Python] Comprehension: list, dict, set, generatorpython
정의 Comprehension(컴프리헨션)은 iterable로부터 list/set/dict/generator를 선언적·간결하게 만드는 문법이다. 일반 + 보다 30-50% 빠르며…
[Python] dataclass: 자동 생성 메서드를 갖춘 데이터 클래스python
정의 (3.7+, PEP 557)는 클래스에 , , 등 상용구 메서드를 자동 생성해주는 데코레이터다. 데이터 컨테이너 클래스를 한 줄 데코레이터 + 필드 어노테이션만으로 만들 수…
[Python] int: 임의 정밀도 정수python
정의 Python의 는 임의 정밀도 정수(arbitrary precision integer)다. C의 / 처럼 비트 폭이 고정되지 않으며, 메모리가 허용하는 한 무한히 큰 정수를…
[Python] Iterator, Generator, yieldpython
Iterator Protocol Iterator는 와 를 구현한 객체. Iterable은 만 가지면 된다 (iterator를 반환). 문은 내부적으로 로 iterator를 얻고 …
[Python] TypedDict, Protocol, runtime_checkablepython
TypedDict (PEP 589, 3.8+) dict의 키와 값 타입을 명시. JSON API 응답이나 설정처럼 dict로 들어오지만 스키마가 정해진 경우. 런타임엔 그냥 di…
[Python] typing: 타입 힌트 기초python
정의 Python 타입 힌트(PEP 484)는 런타임에 강제되지 않는 정적 어노테이션이다. , , 같은 타입 체커가 정적 분석에 사용. 런타임은 어노테이션을 무시 (단, 일부 프…
Pythonpython
정의 Python은 Guido van Rossum이 1991년 발표한 인터프리터 방식의 동적 타이핑 언어다. 읽기 쉬운 문법과 방대한 라이브러리 생태계로 웹 백엔드, 데이터 분석…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기