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

Как коллекции используют память?

1.0 Junior🔥 141 комментариев
#Python Core

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

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

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

Как коллекции используют память в Python

Это важный вопрос для написания эффективного кода. Разные структуры данных используют память по-разному, и выбор неправильной может привести к утечкам памяти и медленному приложению.

Измерение памяти в Python

import sys

# Размер объекта в байтах
my_list = [1, 2, 3]
print(sys.getsizeof(my_list))  # 56 байт (для пустого списка)

# Но это только размер контейнера!
# Объекты внутри занимают отдельно:
for item in my_list:
    print(f"Item {item} size: {sys.getsizeof(item)} bytes")
    # Item 1 size: 28 bytes
    # Item 2 size: 28 bytes
    # Item 3 size: 28 bytes

# Полный размер (контейнер + содержимое)
total = sys.getsizeof(my_list) + sum(sys.getsizeof(item) for item in my_list)
print(f"Total: {total} bytes")  # 140 bytes

Списки (List)

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

import sys

# Пустой список
my_list = []
print(f"Empty list: {sys.getsizeof(my_list)} bytes")  # 56 bytes

# Добавляем элементы
for i in range(10):
    my_list.append(i)
    print(f"List with {i+1} items: {sys.getsizeof(my_list)} bytes")

# Вывод покажет не линейный рост, а скачки:
# Empty list: 56 bytes
# List with 1 items: 88 bytes (зарезервировано на 4 элемента)
# List with 2 items: 88 bytes
# List with 3 items: 88 bytes
# List with 4 items: 88 bytes
# List with 5 items: 120 bytes (зарезервировано на 8 элементов)
# ...

Это похоже на бронирование мест в ресторане:

  • Приходит 1 человек → бронируем на 4
  • Приходит 5 человек → бронируем на 8
  • Приходит 9 человек → бронируем на 16

Проблема: частое добавление элементов

# ❌ Медленно: О(n) операций переаллокации
result = []
for i in range(1_000_000):
    result.append(i)  # Каждое k-е присоединение требует копирования всего списка

# ✅ Быстро: заранее выделить память
result = [0] * 1_000_000
for i in range(1_000_000):
    result[i] = i

Кортежи (Tuple)

Кортежи — иммутабельные, занимают меньше памяти чем списки (нет резервирования).

import sys

my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)

print(f"List: {sys.getsizeof(my_list)} bytes")   # 88 bytes (зарезервировано)
print(f"Tuple: {sys.getsizeof(my_tuple)} bytes")  # 80 bytes (ровно столько, сколько нужно)

# Если в приложении нужны только чтения — используй tuple
# Экономия памяти + нет случайных модификаций

Словари (Dict)

Словари используют hash таблицы, которые тоже резервируют место для избежания коллизий.

import sys

my_dict = {}
print(f"Empty dict: {sys.getsizeof(my_dict)} bytes")  # 240 bytes (много!)

# Добавляем элементы
for i in range(10):
    my_dict[f"key_{i}"] = f"value_{i}"
    print(f"Dict with {i+1} items: {sys.getsizeof(my_dict)} bytes")

# Вывод:
# Empty dict: 240 bytes
# Dict with 1 items: 240 bytes
# Dict with 2 items: 240 bytes
# ...
# Dict with 9 items: 240 bytes
# Dict with 10 items: 368 bytes (зарезервировано на 32 элемента)

Проблема: словари для мутации очень сильно фрагментируют память

# ❌ Плохо: частое добавление/удаление ключей
config = {}
for i in range(100):
    config[f"key_{i}"] = i
for i in range(50):
    del config[f"key_{i}"]  # Фрагментация!
for i in range(100, 150):
    config[f"key_{i}"] = i

# ✅ Лучше: если структура известна, создать один раз
config = {f"key_{i}": i for i in range(150)}

Множества (Set)

Множества тоже hash таблицы, но хранят только ключи (быстрый поиск).

import sys

my_set = {1, 2, 3, 4, 5}
my_list = [1, 2, 3, 4, 5]

print(f"List: {sys.getsizeof(my_list)} bytes")    # 88 bytes
print(f"Set: {sys.getsizeof(my_set)} bytes")      # 216 bytes (больше!)

# НО поиск в set О(1) vs list О(n)
# Для 1000+ элементов set выгоднее

# ✅ Правильное использование set:
allowed_ids = {1, 2, 3, 4, 5}  # Готовимся к О(1) поиску
if user_id in allowed_ids:  # Очень быстро
    pass

Сравнение: List vs Tuple vs Set vs Dict

import sys

n = 1000
items = list(range(n))

my_list = items
my_tuple = tuple(items)
my_set = set(items)
my_dict = {i: i for i in items}

print(f"List:   {sys.getsizeof(my_list):7d} bytes  | Поиск: O(n)  | Мутация: быстро")
print(f"Tuple:  {sys.getsizeof(my_tuple):7d} bytes  | Поиск: O(n)  | Мутация: нельзя")
print(f"Set:    {sys.getsizeof(my_set):7d} bytes   | Поиск: O(1)  | Мутация: медленно")
print(f"Dict:   {sys.getsizeof(my_dict):7d} bytes  | Поиск: O(1)  | Мутация: медленно")

# Вывод:
# List:    9016 bytes  | Поиск: O(n)  | Мутация: быстро
# Tuple:   8024 bytes  | Поиск: O(n)  | Мутация: нельзя
# Set:    32872 bytes  | Поиск: O(1)  | Мутация: медленно
# Dict:   36960 bytes  | Поиск: O(1)  | Мутация: медленно

Глубокие копии vs поверхностные

import copy
import sys

original = [[1, 2, 3] for _ in range(1000)]

# Поверхностная копия (ссылаются на одни и те же подсписки)
shallow = copy.copy(original)
print(f"Shallow copy size: {sys.getsizeof(shallow)} bytes")
shallow[0][0] = 999
print(f"original[0][0] = {original[0][0]}")  # 999! Изменился оригинал!

# Глубокая копия (независимые подсписки)
deep = copy.deepcopy(original)
print(f"Deep copy size: {sys.getsizeof(deep)} bytes")  # Намного больше!
deep[0][0] = 999
print(f"original[0][0] = {original[0][0]}")  # 1 - не изменился

Проблема: случайная глубокая копия может исчерпать память

# ❌ Опасно: глубокая копия больших структур
data = [[1] * 10000 for _ in range(10000)]  # 100M элементов
data_copy = copy.deepcopy(data)  # Выделяет ещё 100M элементов!

# ✅ Лучше: если нужна просто ссылка
data_ref = data  # Ссылка, не копия
data_slice = data[:100]  # Поверхностная копия

Утечки памяти: циклические ссылки

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
        self.parent = None

node1 = Node(1)
node2 = Node(2)

# Циклическая ссылка
node1.next = node2
node2.parent = node1  # node1 -> node2 -> node1

del node1
del node2
# ❌ Память НЕ освобождена! Garbage collector должен её очистить
# но циклические ссылки замедляют GC

# ✅ Лучше: использовать weakref для обратных ссылок
import weakref

class NodeGood:
    def __init__(self, value):
        self.value = value
        self.next = None
        self.parent = weakref.ref(None)  # Слабая ссылка!

node1 = NodeGood(1)
node2 = NodeGood(2)
node1.next = node2
node2.parent = weakref.ref(node1)  # Не держит node1 в памяти

del node1
del node2
# ✅ Память освобождена сразу!

Оптимизация: использование slots

# ❌ Обычный класс использует __dict__ для хранения атрибутов
class PersonNormal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# ✅ С __slots__ занимает намного меньше памяти
class PersonOptimized:
    __slots__ = ['name', 'age']
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

import sys

p1 = PersonNormal("John", 25)
p2 = PersonOptimized("John", 25)

print(f"Normal: {sys.getsizeof(p1.__dict__)} bytes for __dict__")
print(f"With slots: no __dict__ (экономия памяти)")

# Если у вас 1 млн объектов:
# Normal: 1_000_000 * (объект + __dict__) = много памяти
# Optimized: 1_000_000 * (объект) = меньше памяти

Профилирование памяти

import tracemalloc

tracemalloc.start()

# Ваш код
my_list = [i**2 for i in range(1000000)]

current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.2f} MB")
print(f"Peak: {peak / 1024 / 1024:.2f} MB")
tracemalloc.stop()

# Или используй memory_profiler
# pip install memory-profiler
# python -m memory_profiler script.py

Итог: выбор правильной структуры

СтруктураПамятьПоискМутацияКогда использовать
ListЭкономнаяO(n)БыстроПоследовательность, много добавлений
TupleОчень экономнаяO(n)НетConst данные, ключи dict
SetМногоO(1)МедленноПроверка наличия элемента
DictМногоO(1)МедленноХранилище ключ-значение
slotsМинимумO(1)БыстроОбъекты с фиксированными атрибутами

Главное правило: профилируй перед оптимизацией. Выбор неправильной структуры данных может привести к 100x замедлению и утечкам памяти!

Как коллекции используют память? | PrepBro