Поверхностные и глубокие копии списков

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

>>> a = [1, 1, 1]
>>> b = [2, 2, 2]
>>> c = [3, 3, 3]

Теперь, давайте составим из этих векторов следующую матрицу:

>>> m = [a, b, c]
>>> m
[[1, 1, 1],
 [2, 2, 2],
 [3, 3, 3]]

Предположим, что нам нужно выполнить некоторые действия с матрицей m, но при этом векторы a, b и c не должны быть изменены, так как в дальнейшем они будут нужны для выполнения других действий. Изменим матрицу m - заменим ее первый столбец нулями:

>>> m[0][0] = 0
>>> m[1][0] = 0
>>> m[2][0] = 0
>>>
>>> m
[[0, 1, 1],
 [0, 2, 2],
 [0, 3, 3]]

Однако, изменение матрицы m привело к изменению векторов a, b и c, которые мы вовсе не хотели менять:

>>> a
[0, 1, 1]
>>>
>>> b
[0, 2, 2]
>>>
>>> c
[0, 3, 3]

Если изменить элементы в векторах a, b и c то это приведет к изменениям в матрице m:

>>> a[-1] = 0
>>> b[-1] = 0
>>> c[-1] = 0
>>>
>>> m
[[0, 1, 0],
 [0, 2, 0],
 [0, 3, 0]]

Данный пример, как раз и иллюстрирует тот факт, что переменные могут сылаться на одни и те же данные в памяти компьютера. Здесь важно подчеркнуть разницу между "равными" данными и "одними и темиже" данными. Давайте приведем простой пример. Пусть у нас есть вектор a:

>>> a = [0, 0]

И две матрицы m_1 и m_2 следующего вида:

>>> m_1 = [a, [0, 0]]
>>> m_1
[[0, 0],
 [0, 0]]
>>>
>>> m_2 = [[0, 0], a]
>>> m_2
[[0, 0],
 [0, 0]]

Мы можем проверить на равенство вектор a c первой строчкой матрицы m_1 и второй строчкой m_2, получив вполне ожидаемый результат - они равны:

>>>> a == m_1[0], a == m_2[1]
(True, True)

Но мы так же можем проверить являются ли a, m_1[0] и m_2[1] одним и тем же объектом в памяти компьютера:

>>> a is m_1[0], a is m_2[1]
(True, True)

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

>>> m_2[1][0] = 1
>>>
>>> a
[1, 0]
>>>
>>> m_1
[[1, 0], [0, 0]]

В то же время, m_1[1] и m_2[0] содержат данные, которые являются равными, но не являются общими:

>>> m_1[1] == m_2[0]
True
>>>
>>> m_1[1] is m_2[0]
False

А это значит, что изменение этих данных посредством обращения к переменной m_1[1] никак не отразится на данных, на которые ссылается m_2[0]:

>>> m_1[1][0] = 1
>>> m_1
[[1, 0],
 [1, 0]]
>>>
>>> m_2
[[0, 0],
 [1, 0]]

Поверхностные копиии

Что бы продемонстрировать суть поверхностного копирования, давайте создадим список v_1, который содержит вложенный список:

>>> v_1 = [1, 2, [0, 0]]

Теперь приравняем этот список другой переменной v_2:

>>> v_2 = v_1

Как вы уже догадались, данные переменные ссылаются на одни и те же данные:

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

>>> v_1[0] = 111
>>>
>>> v_2
[111, 2, [0, 0]]
>>>
>>>
>>> v_2[2][1] = 999
>>>
>>> v_1
[111, 2, [0, 999]]

Обратите внимание, на то что изменение затронуло данные которые хранятся как в самих списках, так и во вложенных списках. А теперь давайте выполним поверхностное копирование:

>>> v_2 = v_1[:]

Команды v_1*1 и v_2 + [] тоже выполняют поверхностное копирование, но традиционно принято пользоваться командой v_1[:]. Как вы наверняка догадались, поверхностное копирование называется поверхностным, потому что оно не выполняет копирование данных во вложенных списках.

>>> #  данные которые расположены вне вложенных списков,
>>> #  теперь не являются общими:
>>> v_1[0] = 222
>>>
>>> v_1
[222, 2, [0, 999]]
>>>
>>> v_2
[111, 2, [0, 999]]

Но если изменить данные во вложенном списке v_1, то это приведет к изменению вложенного списка v_2

>>> v_1[2][1] = -1
>>>
>>> v_1
[222, 2, [0, -1]]
>>>
>>> v_2
[111, 2, [0, -1]]

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


Глубокое копирование списков

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

>>> a = [1, [2, [3, 4], 5], 6]
>>> a
[1, [2, [3, 4], 5], 6]

Что бы выполнить глубокое (полное) копирование, сначала нужно импортировать модуль copy:

>>> import copy

Потом, можно выполнять копирование:

>>> b = copy.deepcopy(a)

После такого копирования, никакие изменения данных на любом уровне вложенности, посредством одной переменной никак не отразятся на другой переменной:

>>> a[0] = 111
>>> a[1][0] = 222
>>> a[1][1][0] = 333
>>>
>>> a
[111, [222, [333, 4], 5], 6]
>>>
>>>
>>> b
[1, [2, [3, 4], 5], 6]