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

Как устроены изменяемые объекты в памяти?

2.3 Middle🔥 141 комментариев
#Python Core

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

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

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

Как устроены изменяемые объекты в памяти?

Изменяемые объекты (mutable objects) — это объекты, которые можно изменять после создания (списки, словари, множества, пользовательские классы). Понимание их устройства в памяти критично для избежания ошибок и оптимизации кода.

Базовые понятия

В Python всё — это объект с:

  • id — уникальный идентификатор в памяти (адрес в CPython)
  • type — тип объекта
  • value — значение (содержимое)
x = [1, 2, 3]
print(id(x))      # 140734567890000 — адрес объекта
print(type(x))    # <class 'list'> — тип
print(x)          # [1, 2, 3] — значение

Как список хранится в памяти

Список — это динамический массив указателей:

my_list = [10, 20, 30]

# В памяти структура выглядит так:
# my_list (переменная)
#   ↓
# PyListObject (объект списка)
#   ├─ ob_size = 3 (количество элементов)
#   ├─ ob_capacity = 4 (выделенная память)
#   └─ ob_item → массив указателей
#       ├─ → PyObject(10)
#       ├─ → PyObject(20)
#       └─ → PyObject(30)

Демонстрация с id()

# id() показывает адрес объекта
my_list = [1, 2, 3]
original_id = id(my_list)

my_list.append(4)  # Изменяем список
print(id(my_list) == original_id)  # True! — объект в памяти тот же

my_list = [1, 2, 3, 4]  # Переприсваиваем переменную
print(id(my_list) == original_id)  # False — новый объект

Ссылки на мутабельные объекты

Важно понимать, что переменная — это только ссылка на объект:

a = [1, 2, 3]
b = a  # b указывает на ТОТ ЖЕ объект, что и a

print(id(a) == id(b))  # True

a.append(4)
print(b)  # [1, 2, 3, 4] — b изменилась, потому что указывает на тот же объект!

# Это опасно при копировании:
original = [1, 2, 3]
copy = original  # Не копирование, а ещё одна ссылка!

copy[0] = 999
print(original)  # [999, 2, 3] — original тоже изменилась!

Правильное копирование

# Поверхностное копирование (shallow copy)
import copy
original = [1, 2, 3]
copy_shallow = copy.copy(original)
# или
copy_shallow = original.copy()
copy_shallow[0] = 999
print(original)  # [1, 2, 3] — original не изменилась

# Но для вложенных структур может быть проблема:
original = [[1, 2], [3, 4]]
copy_shallow = original.copy()
copy_shallow[0][0] = 999
print(original)  # [[999, 2], [3, 4]] — внутренние списки всё ещё общие!

# Глубокое копирование (deep copy)
copy_deep = copy.deepcopy(original)
copy_deep[0][0] = 999
print(original)  # [[999, 2], [3, 4]] — original не изменилась

Как память распределяется для списка

my_list = []
print(len(my_list), sys.getsizeof(my_list))  # 0, 56 байт (пустой список)

# Python часто выделяет больше памяти, чем нужно
for i in range(10):
    my_list.append(i)
    print(f"len={len(my_list)}, size={sys.getsizeof(my_list)} bytes")

# Выход примерно:
# len=1, size=88 bytes (выделено под 4 элемента)
# len=2, size=88 bytes
# len=3, size=88 bytes
# len=4, size=88 bytes
# len=5, size=120 bytes (выделено под 8 элементов)
# len=10, size=120 bytes

Словарь в памяти

Словарь использует хеш-таблицу для быстрого поиска O(1):

my_dict = {'a': 1, 'b': 2, 'c': 3}

# В памяти (упрощённо):
# PyDictObject
#   ├─ hash('a') % size → [KeyObject('a'), ValueObject(1)]
#   ├─ hash('b') % size → [KeyObject('b'), ValueObject(2)]
#   └─ hash('c') % size → [KeyObject('c'), ValueObject(3)]

Множество (set)

Множество похоже на словарь, но содержит только ключи (без значений):

my_set = {1, 2, 3}

# В памяти:
# PySetObject → хеш-таблица
#   ├─ hash(1) % size → 1
#   ├─ hash(2) % size → 2
#   └─ hash(3) % size → 3

Пользовательский класс

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = Point(10, 20)
p2 = p1  # Ссылка на тот же объект

print(id(p1) == id(p2))  # True

p1.x = 100
print(p2.x)  # 100 — p2 тоже изменилась!

# В памяти Point содержит __dict__:
print(p1.__dict__)  # {'x': 100, 'y': 20}
print(id(p1.__dict__) == id(p2.__dict__))  # True

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

# ❌ Опасно!
def add_item(item, items=[]):
    items.append(item)
    return items

result1 = add_item(1)  # [1]
result2 = add_item(2)  # [1, 2] — ПРОБЛЕМА!
result3 = add_item(3)  # [1, 2, 3] — список переиспользуется!

print(id(result1) == id(result2) == id(result3))  # True — один и тот же объект!

# ✅ Правильно!
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

result1 = add_item(1)  # [1]
result2 = add_item(2)  # [2] — новый список
result3 = add_item(3)  # [3] — новый список

Оптимизация памяти

import sys

# Целые числа кешируются
a = 256
b = 256
print(id(a) == id(b))  # True (CPython кеширует числа -5..256)

# Но не всегда
a = 257
b = 257
print(id(a) == id(b))  # Может быть False (зависит от контекста)

# Строки также кешируются
a = "hello"
b = "hello"
print(id(a) == id(b))  # True (интернирование строк)

# Списки никогда не переиспользуются
a = []
b = []
print(id(a) == id(b))  # False — разные объекты

Резюме

  • Переменные — это ссылки, а не значения
  • Изменение мутабельного объекта видно через все переменные, указывающие на него
  • Копирование требует явного вызова copy() или copy.deepcopy()
  • Стандартные типы (список, словарь) оптимизированы для скорости, но требуют внимательного использования
  • id() помогает отладить проблемы с памятью
Как устроены изменяемые объекты в памяти? | PrepBro