Операторы присваивания в Python не создают копии объектов, они связывают имена только с объектом. Для неизменяемых объектов это обычно не имеет значения. Но для работы с изменяемыми объектами или наборами изменяемых объектов вы можете искать способ создания «реальных копий» или «клонов» этих объектов.

По сути, вам иногда нужны копии, которые вы можете изменить без одновременной автоматической модификации оригинала. В этой главе поговорим о том, как копировать или «клонировать» объекты в Python и обсудим некоторые из связанных с этим предостережений.

Давайте начнем с рассмотрения того, как копировать встроенные коллекции в python. Встроенные изменяемые коллекции python, такие как списки (list), словари (dict) и наборы (set), могут быть скопированы путем вызова функций создания соответствующего объекта, которой в качестве аргумента передается существующая коллекция:

new_list = list (original_list) 
new_dict = dict (original_dict) 
new_set = set (original_set)

Однако этот метод не будет работать для пользовательских объектов, и, кроме того, он создает только поверхностные копии. Для сложных объектов, таких как списки, словари и наборы, существует важное различие между поверхностным (shallow copy) и глубоким (deep copy) копированием:

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

Глубокая копия делает процесс копирования рекурсивным. Это означает сначала создание нового объекта коллекции, а затем рекурсивное заполнение его копиями дочерних объектов, найденных в оригинале. Копирование объектов таким образом просматривает все дерево объектов, чтобы создать полностью независимый клон исходного объекта и всех его дочерних элементов.

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

Создание поверхносной (неглубокой) копии объекта

В приведенном ниже примере мы создадим новый вложенный список и затем поверхностно скопируем его с помощью функции list ():

»> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] »> ys = list (xs) # Создаем поверхностную копию

Это означает, что ys теперь будет новым и независимым объектом с тем же содержимым, что и xs. Вы можете проверить это, проверив оба объекта:

>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]] 
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Чтобы подтвердить, что ys действительно независим от оригинала, давайте проведем небольшой эксперимент. Вы можете попробовать добавить новый элемент оригиналу (xs), а затем проверить, повлияла ли эта модификация на копию (ys):   »> xs.append ([10, 11, 12]) »> xs [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]] »> ys [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

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

Однако, поскольку мы создали только поверхностную копию исходного списка, ys все еще содержит ссылки на исходные дочерние объекты (дементы спсика), хранящиеся в xs.

Эти элементы не были скопированы. В скопированном списке они были снова указаны как ссылки на оригинылы. Поэтому, когда вы изменяете один из дочерних объектов в xs, эта модификация будет отражена и в ys, потому что оба списка имеют одни и те же дочерние объекты:

>>> xs [1] [0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], [10, 11, 12]] 
>>> ys
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]

В приведенном выше примере мы изменили только xs. Но оказалось, что оба элемента в списках под индексом 1 в xs и ys были изменены. Опять же, это произошло потому, что мы создали только поверхностную копию исходного списка.

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

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

• Каким образом создаются глубокие копии коллекций?

• Как вы можете создавать копии (поверхностные и глубокие) произвольных объектов, включая пользовательские классы?

Ответ на эти вопросы лежит в модуле копирования в стандартной библиотеке python. Этот модуль обеспечивает простой интерфейс для создания поверхностных и глубоких копий объектов python.

Создание глубоких копий

Повторим предыдущий пример копирования списка, но с одной важной разницей. На этот раз мы создадим глубокую копию, используя функцию deepcopy(), определенную в модуле копирования:

>>> import copy
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 
>>> zs = copy.deepcopy(xs)

Если проверить xs и его клон zs, которые мы создали с помощью copy.deepcopy(), вы увидите, что они оба выглядят одинаково, как в предыдущем примере:

>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Однако, если вы внесете изменения в один из дочерних объектов исходного объекта (xs), вы увидите, что эта модификация не повлияет на глубину zs. Оба объекта, оригинал и копия, на этот раз полностью независимы. xs был клонирован рекурсивно, включая все его дочерние объекты:

>>> xs [1] [0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]] 
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Возможно, вам понадобится некоторое время, чтобы с помощью интерпретатора python и поиграться с этими примерами. Получить полное представление о том, как работет копирование и клонирование лучше всего практикуясь на более-менее реальных примерах.

Кстати, вы также можете создавать поверхностные копии, используя функцию в модуле копирования. Функция copy.copy() создает поверхностные копии объектов. Это полезно, если вам нужно четко зафиксировать, что вы создаете поверхностную копию где-то в своем коде. Использование copy.copy() позволяет вам однохначно указать этот факт. Однако для встроенных коллекций общепринятым считается, что достаточно просто использовать функции создания списка (list()), словаря (dict()) и набора (set()) для создания поверхностных копий.

Копирование любых объектов

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

Для этого используем модуль копирования. Его функции copy.copy() и copy.deepcopy() могут использоваться для дублирования любого объекта.

Лучший способ понять, как их использовать, - это простой эксперимент. Основывать это на предыдущем примере копирования списка начнем с определения класса для координат точки в двумерном пространстве:

class Point:
	def __init __ (self, x, y):
	self.x = x 
	self.y = y
	
def __repr __ (self):
	return f'Point ({self.x! r}, {self.y! r}) '

Довольно простая конструкция. Добавлена реализацию метода __repr __(), чтобы мы могли легко проверять объекты, созданные из этого класса, в интерпретаторе python.

Затем мы создадим экземпляр класса Point и затем (поверхностно) скопируем его с помощью модуля копирования:

>>> a = Point(23, 42) 
>>> b = copy.copy(a)

Если мы проверим содержимое исходного объекта Point и его (поверхностной) копии, мы увидим, то, что и ожидаем:

>>> a 
Point (23, 42) 
>>> b 
Point (23, 42) 
>>> a is b 
False

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

Перейдем к более сложному примеру. Определим другой класс для представления 2D-прямоугольников. Сделаем это таким образом, чтобы мы могли создать объект, имеющий более сложную иерархию объектов - прямоугольник. Будем использовать для этого объекты Point для представления своих координат:

class Rectangle:
	def __init __ (self, topleft, rightright):
	self.topleft = topleft 
	self.bottomright = justright
	
def __repr __ (self):
	return (f'Rectangle ({self.topleft! r}, '
                 е '{self.bottomright! г})')
                  Опять же, сначала мы попытаемся создать поверхностную копию экземпляра прямоугольника:

rect = Rectangle(Point(0, 1), Point(5, 6)) 
srect = copy.copy(rect)

Если вы проверите исходный прямоугольник и его копию, вы увидите, насколько хорошо работает метод __repr __ (), и что процесс поверхностной копии отработал должным образом:

>>> rect
Rectangle(Point (0, 1), Point(5, 6)) 
>>> srect
Rectangle(Point (0, 1), Point(5, 6)) 
>>> rect is correct
False

Помните, как пример предыдущего списка иллюстрировал разницу между глубокими и поверхностными копиями? Используем тот же подход здесь. Изменим объект глубже в иерархии объектов, а затем вы увидите это изменение, отраженное в (поверхностной) копии:

>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6)) 
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

Надеюсь, получилось так, как вы ожидали этого. Создам глубокую копию исходного прямоугольника. Затем применим другую модификацию, и вы увидите, какие объекты затронуты:

>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6)) 
>>> rect
Rectangle(Point(999, 1), Point(5, 6)) 
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

Вуаля! На этот раз глубокая копия (drect) полностью независима от оригинальной (rect ) и поверхностной копии (srect). На самом деле эта тема настолько глубокая, что рассмотреть все аспекты в коротких примерах почти невозможно. Поэтому вы можете изучить документацию модуля копирования. Например, интерес для вас могут представлять специальные методы __copy __() и __deepcopy __().

Основные выходы

• Создание поверхностной копии объекта не будет клонировать дочерние объекты. Поэтому копия получится не полностью независима от оригинала.

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

• Вы можете скопировать произвольные объекты (включая пользовательские классы) с помощью модуля копирования.

Источник: “Python Tricks The Book” Dan Bader