Перейти к содержимому
Editor

Copy и deepcopy в Python: почему ваша «копия» вовсе не копия

Средний20 min5 упр.70 XP

Этот урок переведён с английского оригинала.Read in EnglishLeer en españolСообщить об ошибке в переводе

0/5 упр.

Представьте папку, полную документов. Вы делаете копию папки и отдаёте её другу. Но есть подвох: фотографии внутри папки не скопированы. Это те же самые бумажные снимки. Если друг пририсует на одном из них усы, ваш оригинал тоже испорчен.

Именно так по умолчанию работает копирование в Python. Вам кажется, что вы сделали независимую копию, но вложенные объекты по-прежнему общие. Это одна из самых запутанных ошибок в Python, и на ней спотыкаются как новички, так и опытные разработчики.

В этом уроке вы узнаете, чем различаются присваивание, поверхностная копия и глубокая копия. К концу урока вы будете точно знать, когда что использовать, и больше никогда не попадётесь на ошибку с «общей ссылкой».

Почему изменение «копии» меняет оригинал?

Начнём с самого частого сюрприза. Когда вы пишете b = a, вы НЕ создаёте копию. Вы даёте второе имя тому же самому объекту. И a, и b указывают на одни и те же данные в памяти.

Присваивание создаёт псевдоним, а не копию
a = [1, 2, 3]
b = a # NOT a copy - just a second name
b.append(4)
print(a) # [1, 2, 3, 4] - a changed too!
print(b) # [1, 2, 3, 4]
Вывод
[1, 2, 3, 4]
[1, 2, 3, 4]

Так происходит потому, что a и b похожи на две наклейки на одной коробке. Если вы положите что-то в коробку через наклейку b, вы увидите это и через наклейку a.

Проверить, указывают ли две переменные на один объект, можно оператором is или сравнив их id().

Проверяем идентичность объектов с помощью is и id()
a = [1, 2, 3]
b = a
print(a is b) # True - same object
print(id(a)) # Same number...
print(id(b)) # ...as this one
Вывод
True
80036304
80036304

Что такое поверхностная копия?

Поверхностная копия (shallow copy) создаёт новый внешний контейнер, но НЕ копирует объекты внутри него. Новый список независим на верхнем уровне, но все вложенные объекты по-прежнему общие с оригиналом.

Сделать поверхностную копию списка можно несколькими способами.

Три способа сделать поверхностную копию списка
original = [1, 2, 3]
# Three ways to shallow copy a list
copy1 = original.copy() # .copy() method
copy2 = original[:] # slice from start to end
copy3 = list(original) # list() constructor
# They are all independent copies
copy1.append(99)
print(original) # [1, 2, 3] - unchanged!
print(copy1) # [1, 2, 3, 99]
Вывод
[1, 2, 3]
[1, 2, 3, 99]

Для словарей используйте .copy() или dict().

Поверхностная копия словаря
original = {'name': 'Alice', 'age': 25}
copy1 = original.copy() # .copy() method
copy2 = dict(original) # dict() constructor
copy1['age'] = 30
print(original) # {'name': 'Alice', 'age': 25} - unchanged
print(copy1) # {'name': 'Alice', 'age': 30}
Вывод
{'name': 'Alice', 'age': 25}
{'name': 'Alice', 'age': 30}

Выглядит идеально. Но здесь и кроется ловушка. Поверхностная копия копирует только один уровень. Если в вашем списке есть другие списки (или словари), эти внутренние объекты остаются общими.

Ловушка поверхностной копии с вложенными данными
# Shallow copy with nested data - the trap!
original = [[1, 2], [3, 4], [5, 6]]
shallow = original.copy()
# Modifying the outer list is fine
shallow.append([7, 8])
print(len(original)) # 3 - unaffected
print(len(shallow)) # 4 - only shallow changed
# But modifying an INNER list affects both!
shallow[0].append(99)
print(original[0]) # [1, 2, 99] - original changed too!
print(shallow[0]) # [1, 2, 99]
Вывод
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.

Глубокая копия создаёт полностью независимый клон
import copy
original = [[1, 2], [3, 4], [5, 6]]
deep = copy.deepcopy(original)
# Modify the deep copy
deep[0].append(99)
deep.append([7, 8])
print(original) # [[1, 2], [3, 4], [5, 6]] - completely unchanged!
print(deep) # [[1, 2, 99], [3, 4], [5, 6], [7, 8]]
Вывод
[[1, 2], [3, 4], [5, 6]]
[[1, 2, 99], [3, 4], [5, 6], [7, 8]]

Глубокая копия работает и со словарями, в том числе со структурами глубокой вложенности.

Глубокая копия вложенных словарей и списков
import copy
user = {
'name': 'Alice',
'scores': [90, 85, 92],
'profile': {'level': 5, 'badges': ['star', 'gold']},
}
clone = copy.deepcopy(user)
# Modify the clone at every level
clone['name'] = 'Bob'
clone['scores'].append(100)
clone['profile']['badges'].append('diamond')
print(user['name']) # Alice
print(user['scores']) # [90, 85, 92]
print(user['profile']['badges']) # ['star', 'gold']
print(clone['name']) # Bob
print(clone['scores']) # [90, 85, 92, 100]
print(clone['profile']['badges']) # ['star', 'gold', 'diamond']
Вывод
Alice
[90, 85, 92]
['star', 'gold']
Bob
[90, 85, 92, 100]
['star', 'gold', 'diamond']

Под капотом: как устроены ссылки в Python

Чтобы по-настоящему понять копирование, нужно понять, как Python хранит данные. Любые данные находятся где-то в памяти в виде объекта. Переменные являются лишь именами, которые ссылаются на эти объекты (указывают на них).

Присваивание копирует ссылку. Поверхностная копия создаёт новый контейнер с теми же ссылками внутри. Глубокая копия создаёт новый контейнер с новыми ссылками, которые указывают на новые копии всего содержимого.

Наглядно смотрим на ссылки с помощью is
import copy
inner = [1, 2, 3]
original = [inner, inner] # Both slots point to the same inner list
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print(original[0] is original[1]) # True - same inner list
print(shallow[0] is original[0]) # True - shallow shares inner objects
print(deep[0] is original[0]) # False - deep made a new copy
Вывод
True
True
False

Вот сводная таблица трёх подходов.

Итог: присваивание, поверхностная и глубокая копия
# Summary:
# ─────────────────────────────────────────────────────
# Method | New outer? | New inner? | Safe for nested?
# ─────────────────────────────────────────────────────
# b = a | No | No | No
# b = a.copy() | Yes | No | No
# b = deepcopy(a) | Yes | Yes | Yes
# ─────────────────────────────────────────────────────
print("Assignment: same outer, same inner")
print("Shallow copy: new outer, same inner")
print("Deep copy: new outer, new inner")
Вывод
Assignment:   same outer, same inner
Shallow copy: new outer, same inner
Deep copy:    new outer, new inner

Когда какой способ копирования использовать?

Используйте присваивание (b = a), когда вам намеренно нужны два имени для одних и тех же данных. Так часто бывает при передаче данных в функции: обычно вы хотите, чтобы функция работала с оригиналом.

Используйте поверхностную копию, когда у вас плоский список или словарь (без вложенных изменяемых объектов) и нужна независимая копия. Это покрывает большинство повседневных случаев.

Используйте глубокую копию, когда у вас есть вложенные изменяемые объекты (списки списков, словари со списками в значениях и т. п.) и нужен полностью независимый клон. Без неё не обойтись, если вы хотите изменять копию без всякого риска задеть оригинал.


Практические упражнения

Предскажите вывод: псевдоним при присваивании
Угадайте вывод

Внимательно прочитайте код. Что он выведет? Помните, что = для списков создаёт псевдоним, а не копию.

Введите вывод в точности.

a = [10, 20, 30]
b = a
b[1] = 99
print(a)
print(a is b)
Сделайте независимую копию
Исправьте ошибку

Код ниже пытается сохранить резервную копию исходного списка перед тем, как его изменить. Но резервная копия тоже меняется! Исправьте код так, чтобы резервная копия оставалась прежней.

Ожидаемый вывод:

Original: [1, 2, 3, 4]
Backup: [1, 2, 3]
original = [1, 2, 3]
backup = original
original.append(4)
print(f"Original: {original}")
print(f"Backup: {backup}")
Предскажите вывод: ловушка поверхностной копии
Угадайте вывод

Этот код делает поверхностную копию вложенных данных. Внимательно прочитайте его и предскажите, что будет выведено.

Введите вывод в точности.

original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(5)
print(original)
print(shallow)
Безопасная копия вложенных данных
Напишите код

Создайте полностью независимую копию данных original с помощью глубокого копирования. Затем добавьте балл 100 в список баллов (scores) копии. Выведите баллы оригинала и баллы копии на отдельных строках, чтобы доказать, что они независимы.

original = {
'name': 'Alice',
'scores': [85, 90, 78],
}
# Create a deep copy
# Add 100 to the copy's scores
# Print original scores, then copy's scores
Исправьте ошибку с состоянием игры
Исправьте ошибку

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

Ожидаемый вывод:

Checkpoint inventory: ['sword', 'shield']
Current inventory: ['sword', 'shield', 'potion']
player = {
'name': 'Hero',
'hp': 100,
'inventory': ['sword', 'shield'],
}
# Save checkpoint
checkpoint = player.copy()
# Player picks up a potion
player['inventory'].append('potion')
print(f"Checkpoint inventory: {checkpoint['inventory']}")
print(f"Current inventory: {player['inventory']}")

Похожие уроки

Урок был полезен?