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

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

Assignment statements in Python do not create copies объектов, они только связывают имена с объектом. Для неизменяемых объектов это обычно не имеет значения.

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

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

Note: Это руководство было написано с учетом Python 3, но между Python 2 и 3 мало различий в том, что касается копирования объектов. Когда будут различия, я укажу их в тексте.

Давайте начнем с рассмотрения того, как копировать встроенные коллекции Python. Встроенные изменяемые коллекции Python, такие какlists, dicts, and sets, можно скопировать, вызвав их фабричные функции для существующей коллекции:

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

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

  • shallow copy означает создание нового объекта коллекции и последующее заполнение его ссылками на дочерние объекты, найденные в оригинале. По сути, неглубокая копия - это толькоone level deep. Процесс копирования не повторяется и поэтому не создает сами копии дочерних объектов.

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

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

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book, который демонстрирует вам лучшие практики Python на простых примерах, которые вы можете мгновенно применить для написания более красивого кода Pythonic.

Изготовление мелких копий

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

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

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

>>>

Как видите, эффект оказался ожидаемым. Изменение скопированного списка на «поверхностном» уровне не составило никаких проблем.

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

Эти дети были скопированыnot. Они просто снова упоминались в скопированном списке.

Следовательно, когда вы изменяете один из дочерних объектов вxs, это изменение также будет отражено вys - это потому, чтоboth lists share the same child objects. Копия является только поверхностной копией одного уровня:

>>>

>>> xs[1][0] = 'X'
>>> xs
[[X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[X', 5, 6], [7, 8, 9]]

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

Если бы мы создали копиюdeepxs на первом этапе, оба объекта были бы полностью независимыми. В этом практическая разница между мелкими и глубокими копиями объектов.

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

  • Как вы можете создавать глубокие копии встроенных коллекций?

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

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

Делать глубокие копии

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

>>>

>>> import copy
>>> xs = [[zs = copy.deepcopy(xs)

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

>>>

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

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

>>>

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

Кстати, вы также можете создавать неглубокие копии, используя функцию в модулеcopy. Функцияcopy.copy() создает мелкие копии объектов.

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

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

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

И снова нам на помощь приходит модульcopy. Его функции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.

Note: В приведенном выше примереPython 3.6 f-string используется для создания строки, возвращаемой__repr__. В Python 2 и версиях Python 3 до 3.6 вы использовали другое выражение для форматирования строки, например:

def __repr__(self):
    return 'Point(%r, %r)' % (self.x, self.y)

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

>>>

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

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

>>>

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

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

Давайте перейдем к более сложному примеру. Я собираюсь определить другой класс для представления 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})')

Опять же, сначала мы попытаемся создать поверхностную копию экземпляра прямоугольника:

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 srect
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 module documentation. Например, объекты могут управлять тем, как они копируются, определяя для них специальные методы__copy__() и__deepcopy__().

3 вещи для запоминания

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

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

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

Если вы хотите глубже изучить другие методы программирования Python среднего уровня, воспользуйтесь этим бесплатным бонусом:

Free Bonus:Click here to get access to a chapter from Python Tricks: The Book, который демонстрирует вам лучшие практики Python на простых примерах, которые вы можете мгновенно применить для написания более красивого кода Pythonic.