Copy и deepcopy в Python: почему ваша «копия» вовсе не копия
Этот урок переведён с английского оригинала.Read in EnglishLeer en españolСообщить об ошибке в переводе
Представьте папку, полную документов. Вы делаете копию папки и отдаёте её другу. Но есть подвох: фотографии внутри папки не скопированы. Это те же самые бумажные снимки. Если друг пририсует на одном из них усы, ваш оригинал тоже испорчен.
Именно так по умолчанию работает копирование в Python. Вам кажется, что вы сделали независимую копию, но вложенные объекты по-прежнему общие. Это одна из самых запутанных ошибок в Python, и на ней спотыкаются как новички, так и опытные разработчики.
В этом уроке вы узнаете, чем различаются присваивание, поверхностная копия и глубокая копия. К концу урока вы будете точно знать, когда что использовать, и больше никогда не попадётесь на ошибку с «общей ссылкой».
Почему изменение «копии» меняет оригинал?
Начнём с самого частого сюрприза. Когда вы пишете b = a, вы НЕ создаёте копию. Вы даёте второе имя тому же самому объекту. И a, и b указывают на одни и те же данные в памяти.
[1, 2, 3, 4] [1, 2, 3, 4]
Так происходит потому, что a и b похожи на две наклейки на одной коробке. Если вы положите что-то в коробку через наклейку b, вы увидите это и через наклейку a.
Проверить, указывают ли две переменные на один объект, можно оператором is или сравнив их id().
True 80036304 80036304
Что такое поверхностная копия?
Поверхностная копия (shallow copy) создаёт новый внешний контейнер, но НЕ копирует объекты внутри него. Новый список независим на верхнем уровне, но все вложенные объекты по-прежнему общие с оригиналом.
Сделать поверхностную копию списка можно несколькими способами.
[1, 2, 3] [1, 2, 3, 99]
Для словарей используйте .copy() или dict().
{'name': 'Alice', 'age': 25}
{'name': 'Alice', 'age': 30}Выглядит идеально. Но здесь и кроется ловушка. Поверхностная копия копирует только один уровень. Если в вашем списке есть другие списки (или словари), эти внутренние объекты остаются общими.
3 4 [1, 2, 99] [1, 2, 99]
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]]import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original) # [[1, 2], [3, 4]]Что такое глубокая копия и когда она нужна?
Глубокая копия (deep copy) создаёт полностью независимый клон. Она копирует внешний объект И рекурсивно копирует каждый объект внутри него, на любой глубине вложенности. У оригинала и копии нет ничего общего.
Чтобы сделать глубокую копию, понадобится модуль copy из стандартной библиотеки Python.
[[1, 2], [3, 4], [5, 6]] [[1, 2, 99], [3, 4], [5, 6], [7, 8]]
Глубокая копия работает и со словарями, в том числе со структурами глубокой вложенности.
Alice [90, 85, 92] ['star', 'gold'] Bob [90, 85, 92, 100] ['star', 'gold', 'diamond']
Под капотом: как устроены ссылки в Python
Чтобы по-настоящему понять копирование, нужно понять, как Python хранит данные. Любые данные находятся где-то в памяти в виде объекта. Переменные являются лишь именами, которые ссылаются на эти объекты (указывают на них).
Присваивание копирует ссылку. Поверхностная копия создаёт новый контейнер с теми же ссылками внутри. Глубокая копия создаёт новый контейнер с новыми ссылками, которые указывают на новые копии всего содержимого.
True True False
Вот сводная таблица трёх подходов.
Assignment: same outer, same inner Shallow copy: new outer, same inner Deep copy: new outer, new inner
Когда какой способ копирования использовать?
Используйте присваивание (b = a), когда вам намеренно нужны два имени для одних и тех же данных. Так часто бывает при передаче данных в функции: обычно вы хотите, чтобы функция работала с оригиналом.
Используйте поверхностную копию, когда у вас плоский список или словарь (без вложенных изменяемых объектов) и нужна независимая копия. Это покрывает большинство повседневных случаев.
Используйте глубокую копию, когда у вас есть вложенные изменяемые объекты (списки списков, словари со списками в значениях и т. п.) и нужен полностью независимый клон. Без неё не обойтись, если вы хотите изменять копию без всякого риска задеть оригинал.
Практические упражнения
Внимательно прочитайте код. Что он выведет? Помните, что = для списков создаёт псевдоним, а не копию.
Введите вывод в точности.
Код ниже пытается сохранить резервную копию исходного списка перед тем, как его изменить. Но резервная копия тоже меняется! Исправьте код так, чтобы резервная копия оставалась прежней.
Ожидаемый вывод:
Original: [1, 2, 3, 4]
Backup: [1, 2, 3]Этот код делает поверхностную копию вложенных данных. Внимательно прочитайте его и предскажите, что будет выведено.
Введите вывод в точности.
Создайте полностью независимую копию данных original с помощью глубокого копирования. Затем добавьте балл 100 в список баллов (scores) копии. Выведите баллы оригинала и баллы копии на отдельных строках, чтобы доказать, что они независимы.
Эта игра сохраняет контрольную точку с состоянием игрока, после чего игрок подбирает предмет. Но когда мы загружаем контрольную точку, предмет уже там! Исправьте код так, чтобы контрольная точка была по-настоящему независимым снимком.
Ожидаемый вывод:
Checkpoint inventory: ['sword', 'shield']
Current inventory: ['sword', 'shield', 'potion']