← Назад к вопросам
Почему кортеж потребляет меньше памяти, чем список в 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 element | 8 байт | 8+ байт (с резервом) |
| Изменяемость | Immutable | Mutable |
| Кеширование | Да | Нет |
| Можно использовать как ключ | Да | Нет |
| Speed append() | N/A | O(1) amortized |
Кортежи потребляют меньше памяти благодаря:
- Отсутствию необходимости в резервировании памяти
- Встроенному хранилищу элементов (inline storage)
- Оптимизациям Python для immutable объектов
- Отсутствию служебных данных для управления размером
Однако списки жертвуют памятью для получения удобства и производительности при добавлении элементов.