Функции copy() и deepcopy() модуля copy

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Следующий модуль, который мы рассмотрим из стандартной библиотеки языка Python, это copy. Он позволяет создавать копии произвольных объектов. О чем здесь речь? Давайте предположим, что в программе имеются следующие объекты:

import copy
 
lst = [1, 2, 3, 4]  # список
d = {'k': 1, 'v': 2}  # словарь
tp = ('a', 'b', 'c')  # кортеж
s = "hello"  # строка
a = 5  # число

Тогда их копии можно попробовать создать с помощью функции copy() модуля copy следующим образом:

lst_cp = copy.copy(lst)
d_cp = copy.copy(d)
tp_cp = copy.copy(tp)
s_cp = copy.copy(s)
a_cp = copy.copy(a)

Давайте проверим, действительно ли везде создаются копии и какие данные хранят новые объекты:

print(f"lst_cp: {id(lst_cp) == id(lst)}, {lst_cp}")
print(f"d_cp: {id(d_cp) == id(d)}, {d_cp}")
print(f"tp_cp: {id(tp_cp) == id(tp)}, {tp_cp}")
print(f"s_cp: {id(s_cp) == id(s)}, {s_cp}")
print(f"a_cp: {id(a_cp) == id(a)}, {a_cp}")

После выполнения программы увидим следующую информацию:

lst_cp: False, [1, 2, 3, 4]
d_cp: False, {'k': 1, 'v': 2}
tp_cp: True, ('a', 'b', 'c')
s_cp: True, hello
a_cp: True, 5

Как видите, копии создаются только для изменяемых объектов (списка и словаря). Неизменяемые (кортеж, строка, число) не дублируются, просто возвращается ссылка на них и переменные tp_cp, s_cp, a_cp ссылаются на те же самые объекты, что и переменные tp, s, a.

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

lst2D = [[1, 2, 3], [4, 5, 6, 7], [8, 9]]

Применяя функцию copy() к таким данным:

lst_c = copy.copy(lst2D)

получим следующий результат:

То есть, вложенные элементы списка будут общими для обоих списков lst2D и lst_c. И мы в этом можем легко убедиться. Изменим какое-либо значение во вложенном списке:

lst2D[0][0] = 10

и оно сказывается на содержимом списка lst_c:

print(lst_c) # [[10, 2, 3], [4, 5, 6, 7], [8, 9]]

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

Функция deepcopy()

Если же нам нужно построить полную копию иерархического объекта (вроде вложенного списка), то следует использовать другую функцию deepcopy() – глубокое копирование. Эта функция по рекурсии перебирает все вложенные объекты и создает их копии. В результате выполнения команды:

lst_c = copy.deepcopy(lst2D)

картина копирования будет выглядеть следующим образом:

Имеем два совершенно разных вложенных списка с полностью независимыми данными. И теперь, изменение какого-либо значения в списке lst2D:

lst2D[0][0] = 10

никак не сказывается на списке lst_c:

print(lst_c) # [[1, 2, 3], [4, 5, 6, 7], [8, 9]]

Функция deepcopy() также успешно справляется с копированием и более сложно устроенных данных. Например:

obj1 = ({1, 2}, [8, 9])
obj2 = ((1, 2, ['a', 'b']), {'key1': 1, 'key2': 2})
 
obj_c1 = copy.deepcopy(obj1)
obj_c2 = copy.deepcopy(obj2)

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

Копирование объектов классов

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    def __repr__(self):
        return f"{id(self)}: {self.x, self.y}: {id(self.x), id(self.y)}"

И создается копия его объекта командой:

pt = Point(10, 20) # исходный объект
pt_cp = copy.copy(pt) # копия объекта pt

Что в итоге получим? Выведем оба объекта в консоль:

print(pt)
print(pt_cp)

Увидим строчки:

2820474102416: (10, 20): (140705615017032, 140705615017352)
2820474102544: (10, 20): (140705615017032, 140705615017352)

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

Функция deepcopy() для объекта pt даст ровно такой же результат.

Конечно, в данном примере переменные x, y – неизменяемые и то, что они не копируются больших проблем, скорее всего, не вызовет. Но, что будет, если вместо конкретных значений передавать список координат? Давайте посмотрим. Перепишем класс Point в виде:

class Point:
    def __init__(self, *crds):
        self.crds = list(crds)
 
    def __repr__(self):
        return f"{id(self)}: {self.crds}: {id(self.crds)}"

Создадим объект этого класса с его копией:

pt = Point(1, 2, 3)  # crds = [1, 2, 3]
pt_cp = copy.copy(pt)

Выведем объекты в консоль:

print(pt)
print(pt_cp)

получим:

1324518367376: [1, 2, 3]: 1324518367488
1324518369808: [1, 2, 3]: 1324518367488

У нас снова два разных объекта, но оба ссылаются на один и тот же список. Это значит, что изменение координат в одном объекте:

pt.crds[0] = 10

приведет к соответствующему изменению в другом объекте pt_cp.

Может быть функция deepcopy() исправит ситуацию и в полном объеме продублирует объект pt? Давайте попробуем:

pt_cp = copy.deepcopy(pt)

Видим, что теперь списки в объектах тоже разные:

2495598124560: [1, 2, 3]: 2495598125248
2495598125328: [1, 2, 3]: 2495598125376

То есть, стандартное поведение функции deepcopy() действительно позволяет создавать глубокие копии объектов классов.

На самом деле, функция deepcopy() перебирает словарь __dict__ с набором локальных атрибутов объекта и при необходимости создает копии объектов, на которые они ссылаются.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме