← Назад к вопросам

Почему кортеж потребляет меньше памяти, чем список в Python?

1.8 Middle🔥 171 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему кортеж потребляет меньше памяти, чем список

Это один из самых популярных вопросов на собеседованиях по Python. Ответ кроется в реализации этих структур в CPython и оптимизациях для immutable объектов.

Основные причины

1. Резерв памяти для будущего роста (Over-allocation)

Список должен быть готов к расширению, поэтому он заранее выделяет больше памяти, чем нужно.

import sys

# Кортеж из 3 элементов
my_tuple = (1, 2, 3)
print(f"Размер кортежа: {sys.getsizeof(my_tuple)} байт")
# Вывод: 48 байт

# Список из 3 элементов
my_list = [1, 2, 3]
print(f"Размер списка: {sys.getsizeof(my_list)} байт")
# Вывод: 56 байт

# На первый взгляд разница мала (8 байт), но...
# Давайте посмотрим на растущий список
small_list = []
for i in range(10):
    small_list.append(i)
    print(f"Элементы: {len(small_list)}, Выделено памяти: {sys.getsizeof(small_list)} байт")

# Вывод:
# Элементы: 1, Выделено памяти: 56 байт
# Элементы: 2, Выделено памяти: 56 байт  (память уже выделена)
# Элементы: 3, Выделено памяти: 56 байт
# Элементы: 4, Выделено памяти: 56 байт
# Элементы: 5, Выделено памяти: 88 байт  (перераспределение! +32 байта)
# ...

Алгоритм роста списка в CPython:

# Примерно так реализована стратегия расширения списка
def calculate_new_size(current_size):
    """Как CPython решает, на сколько расширить список"""
    if current_size == 0:
        return 4  # Начинаем с 4 элементов
    
    # Формула: увеличиваем примерно на 12.5% (или (size >> 3) + (size < 9 ? 3 : 6))
    return current_size + (current_size >> 3) + (3 if current_size < 9 else 6)

# Примеры
print(calculate_new_size(0))   # 4
print(calculate_new_size(4))   # 7
print(calculate_new_size(7))   # 11
print(calculate_new_size(11))  # 17
print(calculate_new_size(100)) # 112

2. Структура данных в памяти

Получим реальную картину через ctypes:

import sys
from ctypes import py_object, pythonapi

def analyze_object_size():
    # Пустой кортеж
    empty_tuple = ()
    print(f"Пустой кортеж: {sys.getsizeof(empty_tuple)} байт")
    # 24 байта
    
    # Пустой список
    empty_list = []
    print(f"Пустой список: {sys.getsizeof(empty_list)} байт")
    # 56 байт (уже зарезервировано место)
    
    # С элементами
    tuple_with_items = (1, 2, 3, 4, 5)
    list_with_items = [1, 2, 3, 4, 5]
    
    print(f"\nКортеж (5 элементов): {sys.getsizeof(tuple_with_items)} байт")
    print(f"Список (5 элементов): {sys.getsizeof(list_with_items)} байт")
    
    # Кортеж: 24 (базовый размер объекта) + 8*5 (указатели) = 64 байта
    # Список: 56 (базовый + резерв) + 8*4 (резервированные указатели) = 88 байт

analyze_object_size()

3. Структура в CPython на уровне C

/* Список в CPython */
typedef struct {
    PyObject_HEAD  // refcount, type, hash
    Py_ssize_t ob_size;  // текущее количество элементов
    PyObject **ob_item;  // указатель на массив указателей
} PyListObject;

/* Кортеж в CPython */
typedef struct {
    PyObject_HEAD  // refcount, type
    Py_ssize_t ob_size;  // количество элементов (не может менять)
    PyObject *ob_item[1];  // встроенный массив (оптимизация)
} PyTupleObject;

Ключевые различия:

  • Список: выделяет память динамически и заранее резервирует место
  • Кортеж: встраивает элементы прямо в структуру объекта (inline storage)

4. Оптимизация для immutable объектов

Кортежи неизменяемы, поэтому Python может применять оптимизации:

# Кеширование кортежей
t1 = (1, 2, 3)
t2 = (1, 2, 3)
print(id(t1) == id(t2))  # Может быть True! (кеш для малых кортежей)

# Списки всегда разные объекты
l1 = [1, 2, 3]
l2 = [1, 2, 3]
print(id(l1) == id(l2))  # False

# Кортежи как ключи словаря
dict_with_tuples = {}
dict_with_tuples[(1, 2)] = "значение"  # ✅ Работает

# Списки не могут быть ключами
try:
    dict_with_lists = {}
    dict_with_lists[[1, 2]] = "значение"  # ❌ TypeError
except TypeError as e:
    print(f"Ошибка: {e}")  # unhashable type: list

5. Практическое сравнение памяти

import sys
import gc

def measure_memory_usage():
    gc.collect()  # Очистим мусор
    
    # Создадим большой список и большой кортеж
    size = 10000
    
    # Список
    big_list = list(range(size))
    list_memory = sys.getsizeof(big_list)
    
    # Кортеж
    big_tuple = tuple(range(size))
    tuple_memory = sys.getsizeof(big_tuple)
    
    print(f"Список {size} элементов: {list_memory} байт")
    print(f"Кортеж {size} элементов: {tuple_memory} байт")
    print(f"Разница: {list_memory - tuple_memory} байт")
    print(f"Список использует на {((list_memory - tuple_memory) / tuple_memory * 100):.1f}% больше")

measure_memory_usage()
# Вывод (примерный):
# Список 10000 элементов: 90016 байт
# Кортеж 10000 элементов: 80080 байт
# Разница: 9936 байт
# Список использует на 12.4% больше

6. Почему список резервирует память

# Это сделано для оптимизации операции append()
# Добавление элемента в конец списка:

list_example = []
for i in range(1000000):
    list_example.append(i)  # O(1) amortized thanks to over-allocation

# Без предварительного выделения памяти каждый append() был бы:
# 1. Выделить новую память
# 2. Скопировать все элементы
# 3. Добавить новый элемент
# Это была бы O(n) операция!

# А с резервом это O(1) в среднем (amortized)

7. Когда это имеет значение

# ❌ Плохо: создание огромного списка, когда нужен кортеж
positions = [[0, 1], [1, 2], [2, 3]]  # Много памяти

# ✅ Хорошо: использовать кортежи для статических данных
positions = [(0, 1), (1, 2), (2, 3)]  # Меньше памяти

# ✅ Хорошо: в параметрах функции использовать кортежи
def process_items(*items):  # items - это кортеж
    for item in items:
        print(item)

# ✅ Хорошо: возвращать кортежи для неизменяемых результатов
def get_user_info(user_id):
    return (123, "John", "john@example.com")  # Кортеж, не список

Заключение

КритерийКортежСписок
Базовый размер24 байта56 байт
Per element8 байт8+ байт (с резервом)
ИзменяемостьImmutableMutable
КешированиеДаНет
Можно использовать как ключДаНет
Speed append()N/AO(1) amortized

Кортежи потребляют меньше памяти благодаря:

  1. Отсутствию необходимости в резервировании памяти
  2. Встроенному хранилищу элементов (inline storage)
  3. Оптимизациям Python для immutable объектов
  4. Отсутствию служебных данных для управления размером

Однако списки жертвуют памятью для получения удобства и производительности при добавлении элементов.