튜플 (tuple)

파이썬의 튜플은 불변(immutable)한 시퀀스 자료형으로, 리스트와 유사하지만 변경이 불가능하다는 점에서 차이가 있습니다. 이번 포스팅에서는 튜플의 기본 문법부터 시작하여, CPython에서 튜플이 어떻게 구현되어 있는지에 대해 자세히 살펴보겠습니다.
튜플의 기본 문법
튜플은 여러 개의 값을 하나의 변수에 저장할 수 있는 자료형입니다. 순서가 있고 (Ordered) 불변성 (Immutable) 의 특징이 있습니다.
# 튜플 생성
t1 = (1, 2, 3)
t2 = 4, 5, 6 # 괄호 생략 가능
t3 = (7,) # 요소가 하나일 경우 쉼표 필수
CPython에서의 튜플 구현
CPython은 파이썬의 표준 구현체로, C 언어로 작성되어 있습니다.
튜플은 tupleobject.h tupleobject.c 파일에서 PyTupleObject
라는 이름의 구조체로 아래와 같이 구현되어 있습니다.
typedef struct {
PyObject_VAR_HEAD
/* Cached hash. Initially set to -1. */
Py_hash_t ob_hash;
/* ob_item contains space for 'ob_size' elements.
Items must normally not be NULL, except during construction when
the tuple is not yet visible outside the function that builds it. */
PyObject *ob_item[1];
} PyTupleObject;
PyObject_VAR_HEAD
는 PyVarObject
객체의 매크로로 튜플의 요소 개수를 저장하는 변수입니다.
PyObject *ob_item[1]
는 실제로 튜플의 요소를 저장하는 포인터 배열의 시작점을 나타냅니다. 배열의 크기는 1로 선언되어 있지만, 실제로는 가변 길이 배열로 사용되며, 메모리 할당시 ob_size
에 따라 필요한 만큼의 공간이 할당됩니다.
이러한 방식은 C 언어에서 가변 길이 구조체를 사용하는 일반적인 방법으로 가변길이 구조체(flexible array) 를 통해 확인할 수 있습니다.
Free List
작은 크기의 튜플은 자주 생성되고 소멸되므로, 메모리 할당과 헤제를 반복하는 것은 비효율적입니다. 이를 개선하기 위해 CPython은 Free List 라는 매커니즘을 도입하여 작은 튜플을 재사용합니다.
PyTuple_MAXSAVESIZE
: Free List에 저장될 수 있는 튜플의 최대 크기입니다. 기본값은 20입니다.PyTuple_MAXFREELIST
: 각 크기별로 저장될 수 있는 튜플의 최대 개수입니다. 기본값은 2000입니다.
튜플이 소멸될 때, 해당 크기의 Free List에 여유가 있다면 튜플을 Free List에 추가하고, 새로운 튜플이 필요할 때 Free List에서 재사용합니다.
tuple_alloc(Py_ssize_t size)
{
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
assert(size != 0); // The empty tuple is statically allocated.
Py_ssize_t index = size - 1;
if (index < PyTuple_MAXSAVESIZE) { // 사이즈가 20미만 튜플을 생성할 때
PyTupleObject *op = _Py_FREELIST_POP(PyTupleObject, tuples[index]); // free list에서 꺼냄
if (op != NULL) {
_PyTuple_RESET_HASH_CACHE(op);
return op;
}
}
...
return result;
}
아래 코드를 실행하면 free list를 통해 같은 메모리 블록 재사용을 확인할 수 있습니다.
t1 = tuple([1, 2]) # list → tuple 변환은 내부에서 PySequence_Tuple → PyTuple_New(2) 을 호출
id1 = id(t1)
# dealloc → Free List에 반환
del t1 # refcount=0 → tuple_dealloc → maybe_freelist_push 실행
# 동일 길이 튜플 재생성
t2 = tuple([3, 4]) # 다시 PyTuple_New(2) → Free List에서 pop 되면 같은 블록 재사용
id2 = id(t2)
print(id1 == id2) # 같은 메모리 블록 재사용
empty tuple 처리
빈 튜플은 변경이 불가능하므로, 하나의 인스턴스를 재사용하는 싱글턴 패턴이 적용됩니다.
tuple_get_empty(void)
{
return (PyObject *)&_Py_SINGLETON(tuple_empty);
}
...
if (size == 0) {
return tuple_get_empty();
}
...
Python 인터프리터를 통해 내부적으로 새로운 튜플 객체를 생성하지 않는 것을 볼 수 있습니다.
>>> t = tuple()
>>> id(t)
4389274296
>>> del t
>>> t = tuple()
>>> id(t)
4389274296
>>> t2 = tuple()
>>> id(t2)
4389274296
>>>