Статья

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

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

Но для работы с изменяемыми объектами или коллекциями изменяемых объектов вы, возможно, захотите создать “полные копии” или “клоны” этих объектов.

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

Давайте начнем с рассмотрения того, как копировать встроенные коллекции Python. Встроенные в Python изменяемые коллекции, такие как списки, словари и сеты, могут быть скопированы путем вызова их фабричных функций:
new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)
Однако этот подход не будет работать для пользовательских объектов, также он создает только простые (неглубокие) копии. Для составных объектов, таких как списки, словари и сеты, существует важное различие между простым и глубоким копированием:
  • Неглубокое копирование создает новый объект коллекции и далее заполняет его ссылками на дочерние объекты, найденные в оригинале. По сути, эта копия имеет глубину всего в один уровень. Процесс копирования не повторяется и, следовательно, не создает копий самих дочерних объектов.
  • Глубокое копирование делает процесс копирования рекурсивным. Сначала создается новый объект коллекции, а затем рекурсивно заполняется копиями дочерних объектов, найденных в оригинале. Копирование объекта таким образом обходит все дерево объектов, чтобы создать полностью независимый клон исходного объекта и всех его дочерних элементов.

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

В приведенном ниже примере мы создадим новый вложенный список, а затем скопируем его с помощью фабричной функции list():
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = list(x)  # делаем неглубокую копию
Это означает, что y теперь будет новым и независимым объектом с тем же содержимым, что и x. Вы можете убедиться в этом:
>>> x
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Чтобы удостовериться, что y действительно независим от оригинала, давайте проведем небольшой эксперимент. Можно попробовать добавить новый подсписок к оригиналу (x), а затем проверить, что это изменение не повлияло на копию (y):
>>> x.append(['new list'])
>>> x
[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['new list']]
>>> y
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Но, поскольку мы создали только поверхностную копию исходного списка, y по-прежнему содержит ссылки на исходные дочерние объекты, хранящиеся в x.

Вложенные в список элементы не были скопированы. На них просто снова ссылались в скопированном списке.

Следовательно, когда вы изменяете один из дочерних объектов в x, это изменение также будет отражено в y — потому, что оба списка используют одни и те же дочерние объекты. Это всего лишь неглубокая копия на один уровень глубже:
>>> x[1][0] = 'Python'
>>> x
[[1, 2, 3], ['Python', 5, 6], [7, 8, 9], ['new list']]
>>> y
[[1, 2, 3], ['Python', 5, 6], [7, 8, 9]]
В приведенном выше примере мы внесли изменения только в x. Но оказывается, что оба подсписка с индексом 1 в x и y были изменены. Это произошло потому, что мы создали только поверхностную копию исходного списка.
Если бы мы создали глубокую копию x на первом шаге, оба объекта были бы полностью независимыми. В этом практическая разница между простыми и глубокими копиями объектов.
Теперь вы знаете, как создавать неглубокие копии некоторых встроенных классов коллекций, и знаете разницу между неглубоким и глубоким копированием. Вопросы, на которые мы все еще хотим получить ответы, таковы:
  • Как создать глубокие копии встроенных коллекций?
  • Как создать копии (простые и глубокие) произвольных объектов, включая пользовательские классы?
Ответ на эти вопросы лежит в модуле copy в стандартной библиотеки Python. Этот модуль предоставляет простой интерфейс для создания простых и глубоких копий произвольных объектов Python.

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

Давайте повторим предыдущий пример копирования списка, но с одним важным отличием. На этот раз мы собираемся создать глубокую копию, используя вместо этого функцию deepcopy(), определенную в модуле copy:
>>> import copy
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> z = copy.deepcopy(x)
Когда вы проверите x и его клон z, которые мы создали с помощью copy.deepcopy(), вы увидите, что они оба снова выглядят идентично — точно так же, как в предыдущем примере:
>>> x
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> z
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Однако, если вы внесете изменение в один из дочерних объектов в исходном объекте x, вы увидите, что это изменение не повлияет на глубокую копию z.

Оба объекта, оригинал и копия, на этот раз полностью независимы. x был рекурсивно клонирован, включая все его дочерние объекты:
>>> x[1][0] = 'Питон'
>>> x
[[1, 2, 3], ['Питон', 5, 6], [7, 8, 9]]
>>> z
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Возможно, вам захочется потратить некоторое время на то, чтобы поработать с интерпретатором Python и воспроизвести эти примеры прямо сейчас. Изучать копирование объектов становится легче, когда вы получаете возможность лично ознакомиться с примерами и поиграть с ними.
Кстати, вы также можете создавать неглубокие копии, используя функцию в модуле copy. Функция copy.copy() создает неглубокие копии объектов.
Это полезно, если вам нужно четко сообщить, что вы создаете неглубокую копию где-то в своем коде. Использование copy.copy() позволяет указать на этот факт. Однако для встроенных коллекций считается более питоническим просто использовать функции list, dict и set factory для создания неглубоких копий.

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

Вопрос, на который нам все еще нужно ответить, заключается в том, как мы создаем копии (простые и глубокие) произвольных объектов, включая пользовательские классы.
И снова модуль копирования приходит нам на помощь. Его функции copy.copy() и copy.deepcopy() можно использовать для дублирования любого объекта.
Лучший способ понять, как их использовать, – это провести простой эксперимент. Давайте начнем с определения простого класса 2D-точек:
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, а затем (неглубоко) скопируем его, используя модуль копирования:
>>> p = Point(15, 18)
>>> b = copy.copy(p)
Если мы проверим содержимое исходного объекта и его (неглубокого) клона, мы увидим то, что и следовало ожидать:
>>> p
Point(15, 18)
>>> b
Point(15, 18)
>>> p is b
False
Вот еще кое-что, о чем следует помнить. Поскольку наш объект Point использует неизменяемые типы (ints) для своих координат, в этом случае нет разницы между глубокой и неглубокой копией.

Давайте перейдем к более сложному примеру. Определим другой класс для представления 2D-прямоугольников. Это позволит нам создать более сложную иерархию объектов — прямоугольники будут использовать объекты Point для представления своих координат:
class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def __repr__(self):
        return (f'Rectangle({self.topleft!r}, '
                f'{self.bottomright!r})')
Опять же, сначала мы попытаемся создать неглубокую копию экземпляра Rectangle:
rect = Rectangle(Point(0, 2), Point(7, 9))
nrect = copy.copy(rect)
Если вы проверите исходный прямоугольник и его копию, вы увидите, насколько хорошо работает переопределенный метод __repr__(), и что процесс неглубокого копирования сработал так, как ожидалось:
>>> rect
Rectangle(Point(0, 2), Point(7, 9))
>>> nrect
Rectangle(Point(0, 2), Point(7, 9))
>>> rect is nrect
False
Помните, как предыдущий пример со списком иллюстрировал разницу между глубокими и неглубокими копиями? Здесь мы будем использовать тот же подход. Изменим объект глубже в иерархии объектов, и тогда вы увидите, что это изменение также отражено в (неглубокой) копии:
>>> rect.topleft.x = 33
>>> rect
Rectangle(Point(33, 2), Point(7, 9))
>>> nrect
Rectangle(Point(33, 2), Point(7, 9))
Теперь создадим глубокую копию исходного прямоугольника.
>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 444
>>> drect
Rectangle(Point(444, 2), Point(7, 9))
>>> rect
Rectangle(Point(33, 2), Point(7, 9))
>>> nrect
Rectangle(Point(33, 2), Point(7, 9))
Вуаля! На этот раз глубокая копия (drect) полностью независима от оригинала (rect) и неглубокой копии (nrect).

3 вещи, которые нужно запомнить

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