Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как коллекции используют память в 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 замедлению и утечкам памяти!