Программирование4 сентября 2026 г.

Поверхностная и глубокая копия в Python: как проверить изменение вложенного списка

#COURSE##INNER#

Поверхностная и глубокая копия в Python: как проверить изменение вложенного списка

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

Чтобы быстро это проверить, недостаточно сравнить значения через ==. Нужна проверка ссылок: один и тот же это объект или нет. Для такой диагностики достаточно трех шагов: сравнить контейнеры через is, отдельно сравнить вложенные списки, а затем сделать одну контролируемую правку вложенного элемента и посмотреть, где она проявилась. Ниже разберем этот сценарий на учебном примере в обычном Python 3, который можно запустить в файле .py или в REPL.

Вложенные учебные данные

УЧЕБНЫЙ ПРИМЕР. Возьмем условный список навыков по двум темам. Это именно учебные данные, чтобы было легко увидеть разницу между верхним уровнем списка и вложенными списками:

import copy

source = [
    ["Python", "SQL"],
    ["Git", "Linux"],
]

alias = source
shallow = source.copy()
deep = copy.deepcopy(source)

Здесь source — исходные данные. Переменная alias не создает копию, а просто указывает на тот же самый объект. Переменная shallow получает поверхностную копию: внешний список новый, но вложенные списки внутри него пока общие с исходным. Переменная deep получает глубокую копию: и внешний список, и вложенные списки создаются заново.

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

Ссылки на объекты

В Python присваивание само по себе не копирует объект. Оно лишь связывает новое имя с уже существующим объектом. Поэтому alias = source — это не запасная версия списка, а второе имя для тех же данных. Если изменить вложенный элемент через alias, исходный список тоже изменится, потому что это один и тот же объект.

print(source is alias)      # True
print(source == alias)      # True

alias[0][1] = "Pandas"
print(source)               # [['Python', 'Pandas'], ['Git', 'Linux']]

Здесь полезно разделять две проверки. == спрашивает, равны ли значения. is спрашивает, один и тот же это объект или нет. Для поиска ошибки с вложенными списками важнее именно вторая проверка. Два разных списка могут быть равны по содержимому, но полностью независимы. И наоборот: две переменные могут указывать на один объект, из-за чего изменение через одну переменную сразу видно через другую.

Если вы хотите увидеть это еще нагляднее, можно вывести идентификаторы:

print(id(source), id(alias))
print(id(source[0]), id(alias[0]))

В случае с alias совпадут и идентификатор внешнего списка, и идентификаторы вложенных списков. Это самый простой сигнал, что никакой копии не произошло.

Два варианта копии

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

import copy

source = [
    ["Python", "SQL"],
    ["Git", "Linux"],
]

shallow = source.copy()
deep = copy.deepcopy(source)

print(source is shallow)        # False
print(source[0] is shallow[0])  # True

print(source is deep)           # False
print(source[0] is deep[0])     # False

Это можно представить как простую схему объектов:

source  -> outer A
           A[0] -> inner B
           A[1] -> inner C

shallow -> outer D
           D[0] -> inner B
           D[1] -> inner C

deep    -> outer E
           E[0] -> inner F
           E[1] -> inner G

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

Проверим оба случая на одном наборе данных. Сначала изменим вложенный элемент в поверхностной копии:

shallow[0][1] = "Pandas"
print(source)    # [['Python', 'Pandas'], ['Git', 'Linux']]
print(shallow)   # [['Python', 'Pandas'], ['Git', 'Linux']]

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

shallow[1] = ["Docker", "CI"]
print(source)    # [['Python', 'Pandas'], ['Git', 'Linux']]
print(shallow)   # [['Python', 'Pandas'], ['Docker', 'CI']]

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

Проверка независимости

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

import copy

def inspect_copy(original, candidate):
    print("outer same:", original is candidate)
    for i in range(len(original)):
        print(f"inner {i} same:", original[i] is candidate[i])
    print()

source = [
    ["Python", "SQL"],
    ["Git", "Linux"],
]

shallow = source.copy()
deep = copy.deepcopy(source)

inspect_copy(source, shallow)
inspect_copy(source, deep)

Ожидаемый проверяемый результат для этого учебного примера такой: у shallow строка outer same должна быть False, а проверки inner 0 same и inner 1 same — True. У deep все три проверки должны дать False. Если у вас получился такой вывод, структура ссылок понята верно.

После этого можно сделать контрольное изменение вложенного элемента:

source = [
    ["Python", "SQL"],
    ["Git", "Linux"],
]

shallow = source.copy()
deep = copy.deepcopy(source)

shallow[0][0] = "Flask"
print(source[0][0])   # Flask

deep[1][1] = "Bash"
print(source[1][1])   # Linux

Практический итог простой. Если original[i] is copy_obj[i] возвращает True, изменение элемента внутри этого вложенного списка затронет обе структуры. Если возвращает False, вложенный список независим, и его мутация не должна изменить исходные данные. Для списков списков это и есть короткая рабочая проверка, которую удобно держать под рукой при отладке.

Если тема копирования у вас постоянно всплывает в задачах, полезно отдельно потренировать работу со списками в памяти. В курсе «Программирование на Python 3» во внутренней карточке каталога и на официальной странице подтверждены темы хранения списков в памяти, копирования последовательностей и работы со вложенными списками. Для этой статьи это ценно именно как база для самостоятельной практики с list.copy(), id() и copy.deepcopy().

Курсы

108173
0,0
0 отзывов
Программирование на Python 3
  • В любой момент
  • 4 месяца
  • самостоятельно
  • сертификат
5 390 ₽5 390 ₽
Перейти на сайт