Копирование коллекций

Вспомним что в python все встроенные типы классифицируются на изменяемые и неизменяемые (mutable / immutable):
Вспомним, что когда изменяется содержимое неизменяемого типа - старый объект в памяти уничтожается и создается новый, а в переменную записывается ссылка на новый объект. В случае же с изменяемыми типами - объект не пересоздается. Такое поведение отбрасывает тень на процесс копирования коллекций, особенно вложенных. Давайте рассмотрим подводные камни процесса копирования коллекций.

Рассмотрим 3 случая копирования, на примере списков, которые будут давать разный результат.

1. Копирование присваиванием - создает новую переменную и присваивает ей ссылку на оригинальный объект.

l1 = [1, 2, [3, 4]]
l2 = l1
l1[0] = 'a'
l1.append('z')
print('l1 =', l1)
print('l2 =', l2)
В этом случае будет создана переменная l2 и ей будет присвоена ссылка на тот же объект, на который ссылается l1. Таким образом при изменении одного списка будет изменяться и второй - это может стать большой неожиданностью, когда ваш корабль не приземлится на Марс.


2. Поверхностное копирование коллекции - создает новый объект, и затем вставляет в него ссылки на объекты, находящиеся в оригинале.
l1 = [1, 2, [3, 4]]
l2 = l1.copy() # или l2 = l1[:]
l1[0] = 'a'
l1.append('z')
print('l1 =', l1)
print('l2 =', l2)
Кажется что теперь копирование прошло правильно, но не спешите! В этом случае будет создана поверхностная копия, это значит что в памяти создается новый объект и ссылка на него будет присвоена переменной l2, но элементами списка будут все те же ссылки что и у l1. Как это выстрелит вам в ногу лучше понять на примере:
l1 = [1, 2, [3, 4]]
l2 = l1.copy() # или l2 = l1[:]
l1[0] = 'a'
l1[2].append('z') # изменим вложенный список
print('l1 =', l1)
print('l2 =', l2)
Почему вложенный список изменился? Разберем что пошло не так.

В обоих списках ссылки указывали на одни и те же объекты в памяти. Когда мы выполнили первую операцию:

l1[0] = 'a'

Элемент списка l1[0]=1 представлял неизменяемый тип (int), а значит ссылка на него была удалена, затем был создан новый объекта "new" и ссылка на него была добавлена в список в позицию 0, в это время в l2, элемент с индексом 0 все еще указывает на старый объект "1".
Внешне это сработало так будто вы действительно работаете с разными списками, но на практике оба списка все еще ссылаются на один и тот же набор объектов.

Когда выполнили вторую операцию:

l1[2].append('z')

Вы изменили вложеный список (который является изменяемым типом), он не был пересоздан, он изменился, но на него все так же ссылались, оба списка l1 и l2. Таким образом вы получили ожидаемый результат - значение в обоих списках изменилось.

Это серьезная ошибка которую допускают даже опытные разработчики, не до конца понимающие как работает объектная модель Python.

3. Глубокое копирование коллекции - создает новый составной объект, и затем рекурсивно вставляет в него копии объектов, находящихся в оригинале.
from copy import deepcopy

l1 = [1, 2, [3, 4]]
l2 = deepcopy(l1)
l1[0] = 'a'
l1[2].append('z') # изменим вложенный список
print('l1 =', l1)
print('l2 =', l2)
Для глубокого копирования в стандартной библиотеке существует функция deepcopy из модуля copy.

Вывод: каждый способ копирования имеет свое место применения, понимая тонкости копирования выбирайте метод подходящий для вас. Если ваш список содержит только объекты неизменяемых типов то достаточно поверхностного копирования.

Вывод: если ваша коллекция содержит изменяемый тип - то помните о правилах копирования!

А что, это касается множеств ? - сами множества изменяемые, но они могут содержать только хэшируемые (неизменяемые) объекты, значит с копированием множества нет проблем.