Поверхностная и глубокая копия объектов Python

Присваивание в Python не копирует объект: оно создаёт ещё одно имя для той же ссылки. Поверхностная копия создаёт новый внешний контейнер, но оставляет ссылки на вложенные…

Присваивание в Python не копирует объект: оно создаёт ещё одно имя для той же ссылки. Поверхностная копия создаёт новый внешний контейнер, но оставляет ссылки на вложенные элементы. Глубокая копия рекурсивно копирует составные части и учитывает уже посещённые объекты. Выбор определяется не размером записи, а моделью владения: какие изменения должны быть общими, а какие требуется изолировать.

Три разных операции

После alias = original выражение alias is original истинно. После copy.copy(original) внешний объект новый, но его изменяемые дети могут совпадать по идентичности. После copy.deepcopy(original) обычно новыми становятся и вложенные изменяемые объекты. Неизменяемые значения могут безопасно переиспользоваться даже при глубоком копировании, поэтому не следует требовать новый идентификатор для каждого числа или строки.

Рабочий пример с вложенным списком

from copy import copy, deepcopy

original = {
    'owner': 'Ирина',
    'tasks': [
        {'title': 'тесты', 'done': False},
        {'title': 'документация', 'done': False},
    ],
}

shallow = copy(original)
deep = deepcopy(original)

shallow['tasks'][0]['done'] = True
deep['tasks'][1]['done'] = True

print(original['tasks'][0]['done'])
print(original['tasks'][1]['done'])
print(shallow['tasks'] is original['tasks'])
print(deep['tasks'] is original['tasks'])

Ожидаемый результат и объяснение

True
False
True
False

Словарь shallow новый, однако значение по ключу tasks осталось той же ссылкой. Изменение первого вложенного словаря видно через original. У deep список и словари внутри отделены, поэтому изменение второй задачи не затрагивает источник. Простое выражение dict(original) в этом примере тоже создало бы лишь поверхностную копию.

Циклы, общие ссылки и настройка deepcopy

deepcopy хранит служебную таблицу уже скопированных объектов. Благодаря этому он умеет обходить циклические структуры и сохранять совместное использование: если две ссылки исходного графа указывали на один дочерний объект, соответствующие ссылки копии также должны указывать на один новый дочерний объект. Класс может настроить поведение методом __deepcopy__(self, memo), но тогда обязан корректно использовать memo, иначе легко получить рекурсию или разрушить структуру связей.

Как найти разделяемую вложенную ссылку

Типичная логическая ошибка выглядит не как исключение: разработчик делает copy.copy, меняет вложенный список и замечает изменение оригинала. Для диагностики временно сравните идентичности на каждом уровне: shallow is original, затем shallow['tasks'] is original['tasks'], затем идентичность конкретной задачи. Первый уровень покажет False, второй — True, что точно укажет место разделяемой ссылки.

Обратная ошибка — бездумно применять deepcopy к объектам, представляющим файлы, соединения или внешние ресурсы. Не каждый объект допускает осмысленное копирование. В такой модели лучше явно создать новый доменный объект из нужных данных либо запретить операцию.

Практикум: три копии графа проектов

Создайте список проектов, где каждый проект содержит имя и список участников. Получите псевдоним, поверхностную и глубокую копии. Добавьте участника во вложенный список каждой версии по очереди и запишите, где видно изменение. Самостоятельная проверка: используйте is на внешних списках, словарях проекта и списках участников; перед запуском предскажите каждое значение, а затем объясните результат через граф ссылок, не через фразу «Python иногда копирует».

Границы поверхностного и глубокого копирования

Присваивание разделяет весь объект. Поверхностная копия отделяет только внешний уровень. Глубокая копия обходит граф и отделяет вложенное изменяемое состояние, но может быть дорогой или концептуально неверной для ресурсов. Сначала определите границу владения данными, затем выбирайте способ копирования.

Вопросы о копировании объектов

Срез списка создаёт глубокую копию?

Нет. items[:] создаёт новый список с теми же ссылками на элементы, то есть поверхностную копию.

Всегда ли deepcopy создаёт новый объект для каждого значения?

Нет. Неизменяемые объекты могут переиспользоваться, а общие ссылки внутри графа сохраняют общность уже в копии.

Можно ли заменить deepcopy сериализацией?

Не в общем случае. Сериализация поддерживает ограниченный набор данных, может терять типы и имеет собственные правила восстановления. Это отдельная операция с другим контрактом.

Источники