← Назад к вопросам
Как выделяется память под элементы в списке в Python?
2.0 Middle🔥 201 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Выделение памяти под элементы списка в Python
Это сложный вопрос, который требует понимания того, как Python управляет памятью. В Python всё — это объекты, и список хранит не сами значения, а ссылки на объекты.
Концепция: Object Reference Model
Python использует модель ссылок на объекты, а не прямое хранение значений:
my_list = [1, 2, 3]
# На самом деле это выглядит так:
# my_list = [<ref to int(1)>, <ref to int(2)>, <ref to int(3)>]
# Проверим это
print(id(1)) # 140734829412320 - адрес объекта int(1)
print(id(my_list[0])) # Тот же адрес!
# Это значит, что 1 — это ОДИН объект в памяти
# А список хранит ссылку на этот объект
Структура списка в памяти
# Список — это объект Python, который содержит:
# 1. Размер массива
# 2. Количество элементов
# 3. Массив указателей (PyObject*) на элементы
# На C уровне это выглядит примерно так:
# typedef struct {
# Py_ssize_t ob_size; // Количество элементов
# PyObject **ob_item; // Массив указателей
# } PyListObject;
my_list = [1, 2, 3]
# Выделяется память для:
# 1. Объекта PyListObject (структура)
# 2. Массива указателей (может быть больше, чем 3 элемента из-за over-allocation)
# 3. Сами объекты int(1), int(2), int(3) находятся в другом месте памяти
Over-allocation (Перевыделение памяти)
Список выделяет БОЛЬШЕ памяти, чем нужно, для оптимизации операций append:
import sys
my_list = []
print(f"Пусто: {sys.getsizeof(my_list)} байт")
for i in range(10):
my_list.append(i)
size = sys.getsizeof(my_list)
capacity = len(my_list)
allocated_slots = (size - 56) // 8 # 56 байт на структуру, 8 байт на указатель
print(f"Элементов: {capacity}, Выделено слотов: {allocated_slots}")
# Вывод примерно:
# Элементов: 1, Выделено слотов: 1
# Элементов: 2, Выделено слотов: 2
# Элементов: 3, Выделено слотов: 3
# Элементов: 4, Выделено слотов: 4
# Элементов: 5, Выделено слотов: 8 <- Вот здесь произошло перевыделение!
# Элементов: 6, Выделено слотов: 8
# Элементов: 7, Выделено слотов: 8
# Элементов: 8, Выделено слотов: 8
# Элементов: 9, Выделено слотов: 16
# Элементов: 10, Выделено слотов: 16
Формула роста примерно: new_capacity = capacity + (capacity >> 3) + 3
Память для самих объектов
Объекты хранятся отдельно в heap'е:
import sys
# Каждый объект в Python занимает память
print(sys.getsizeof(1)) # 28 байт (int объект)
print(sys.getsizeof("hello")) # 54 байта (string объект)
print(sys.getsizeof([1, 2, 3])) # 88 байт (list структура)
# Интересная особенность: маленькие целые числа закэшированы
my_list = [1, 1, 1]
print(id(my_list[0])) # 140734829412320
print(id(my_list[1])) # 140734829412320 - ОДИН И ТОТ ЖЕ адрес!
print(id(my_list[2])) # 140734829412320
# Это потому, что Python закэшует целые числа от -5 до 256
print(id(257) == id(257)) # False! Разные объекты
Визуальное представление
┌─────────────────────────────────────┐
│ my_list = [10, "hello", 3.14] │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ PyListObject (в памяти) │
├─────────────────────────────────────┤
│ ob_size: 3 │
│ ob_item: 0x140734829412320 │
└─────────────────────────────────────┘
↓
┌──────────┬──────────┬──────────────┐
│ Массив указателей на элементы │
├──────────┼──────────┼──────────────┤
│ Ref to │ Ref to │ Ref to │
│ int(10) │ str() │ float(3.14) │
└──────────┴──────────┴──────────────┘
↓ ↓ ↓
┌────┐ ┌──────────┐ ┌──────────┐
│ 10 │ │ "hello" │ │ 3.14 │
└────┘ └──────────┘ └──────────┘
(отдельные объекты в памяти)
Dynamically resizable array (Переменный размер)
# Когда мы добавляем элемент, а памяти нет:
my_list = [1, 2, 3] # Выделено 4 слота
# Операция append когда слотов нет:
my_list.append(4) # Перераспределение!
# Python:
# 1. Выделяет НОВЫЙ массив (большего размера)
# 2. Копирует ВСЕ указатели из старого массива
# 3. Добавляет новый элемент
# 4. Освобождает старый массив
# 5. Обновляет указатель my_list.ob_item на новый массив
Дефрагментация памяти
В Python есть garbage collector, который управляет памятью:
import gc
# Когда объект удаляется и на него больше нет ссылок
my_list = [1, 2, 3]
del my_list # Объект удалён, память освобождена
# Garbage collector запускается автоматически
gc.collect() # Вручную запустить сборку мусора
# Проверить утечки памяти
gc.get_stats()
Сравнение с хранением значений
# Python: Хранит ссылки
my_list = [1, 1, 1] # Три ссылки на ОДН объект int(1)
# C: Хранит значения
# int array[3] = {1, 1, 1} // Три отдельные ячейки с значением 1
# Поэтому Python медленнее, но гибче
my_list[0] = "строка" # Можем менять тип!
Reference Counting (Подсчёт ссылок)
import sys
obj = "hello"
print(sys.getrefcount(obj)) # Количество ссылок на объект
my_list = [obj, obj, obj]
print(sys.getrefcount(obj)) # Увеличилось на 3
del my_list
print(sys.getrefcount(obj)) # Уменьшилось
Когда refcount становится 0, объект удаляется и память освобождается.
Оптимизация памяти
# Плохо: много объектов
big_list = [i for i in range(1000000)]
# Лучше: генератор (ленивое вычисление)
big_gen = (i for i in range(1000000))
# Ещё лучше: range (очень эффективно)
big_range = range(1000000)
import sys
print(sys.getsizeof(big_list)) # Много МБ
print(sys.getsizeof(big_gen)) # Несколько байт
print(sys.getsizeof(big_range)) # Несколько байт
Заключение
Память под элементы списка выделяется следующим образом:
- Объект списка занимает фиксированное место (структура PyListObject)
- Массив указателей выделяется с over-allocation для эффективности
- Сами элементы хранятся отдельно в heap'е, как отдельные объекты
- Reference counting отслеживает, используется ли объект
- Garbage collector освобождает неиспользуемую память
Это делает Python гибким, но требует больше памяти, чем языки с прямым хранением значений.