Предполагается, что вы уже умеете писать простые программы на Python и знаете базовый синтаксис (if, for, функции, списки). Здесь мы разбираем не как пользоваться языком, а почему он работает именно так.

Попробуйте угадать, что выведет этот код.

numbers = [[1, 2], [3, 4]]

copy = numbers.copy()

copy[0].append(99)

print(numbers)

Если слово «копия» понимать буквально, то ответ кажется очевидным.

Мы изменили копию.

Значит, оригинал должен остаться прежним.

Многие ожидают увидеть:

[[1, 2], [3, 4]]

Но Python выводит совсем другое:

[[1, 2, 99], [3, 4]]

Получается странная ситуация.

Мы изменяли копию, а изменился оригинал.

Неужели метод copy() работает неправильно?


На самом деле метод работает именно так, как задумывался.

Проблема в другом.

Мы привыкли думать о копировании как о создании полностью независимого объекта.

Но в Python существует несколько видов копирования.

Метод copy() создаёт новый список.

Однако элементы этого списка он не копирует.

Посмотрим, что происходит.

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

numbers = [[1, 2], [3, 4]]

создаются три объекта.

Два внутренних списка:

[1, 2]

[3, 4]

и ещё один внешний список, который хранит ссылки на них.

Схематично это можно представить так:

numbers


┌──────────────┐
│  •      •    │
└──┼──────┼────┘
   │      │
   ▼      ▼
[1, 2]  [3, 4]

Теперь выполняется:

copy = numbers.copy()

Создаётся новый внешний список.

Но его элементы продолжают ссылаться на те же самые внутренние объекты.

Получается такая картина:

numbers ─┐


     ┌──────────────┐
     │  •      •    │
     └──┼──────┼────┘
        │      │
        ▼      ▼
      [1,2]  [3,4]
        ▲      ▲
        │      │
     ┌──┼──────┼────┐
     │  •      •    │
     └──────────────┘


        copy

Обратите внимание.

Внешних списков теперь действительно два.

Но внутренних списков по-прежнему только два.

Именно поэтому строка

copy[0].append(99)

изменяет не “копию”.

Она изменяет внутренний список.

А этот список является общим для обеих коллекций.

Поэтому изменение становится видно через обе переменные.


Может возникнуть вопрос.

Если copy() не копирует всё полностью, зачем он вообще нужен?

Ответ зависит от того, что именно требуется программе.

Во многих случаях достаточно создать новый контейнер.

Например:

numbers = [1, 2, 3]

copy = numbers.copy()

Здесь элементы являются числами.

А числа - неизменяемые объекты.

Поэтому такой копии обычно вполне достаточно.

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

Например:

  • списки;
  • словари;
  • множества.

Именно тогда поверхностного копирования уже недостаточно.


К счастью, Python умеет создавать и полностью независимые копии.

Для этого существует модуль copy и функция deepcopy().

from copy import deepcopy

numbers = [[1, 2], [3, 4]]

copy = deepcopy(numbers)

copy[0].append(99)

print(numbers)

Теперь результат будет таким:

[[1, 2], [3, 4]]

Почему?

Потому что deepcopy() копирует не только внешний список.

Она рекурсивно создаёт копии всех вложенных объектов.

После такого копирования общих внутренних списков уже не остаётся.


Теперь можно сформулировать простое правило.

Поверхностная копия создаёт новый контейнер, но не копирует объекты внутри него.

Глубокая копия создаёт новые объекты на всех уровнях вложенности.


Если вспомнить предыдущие статьи, то никакой новой магии здесь снова нет.

Есть объекты.

Есть ссылки.

Именно ссылки копирует copy().

А deepcopy() создаёт новые объекты вместо того, чтобы использовать существующие.


Мы уже достаточно хорошо научились понимать, как объекты появляются, изменяются и копируются.

Но остаётся ещё один интересный вопрос.

Иногда кажется, что объект уже никому не нужен.

Например:

numbers = [1, 2, 3]

del numbers

Что происходит с самим объектом?

Исчезает ли он сразу?

Или Python хранит его где-то ещё?

Об этом мы поговорим в следующей статье.