Понимание копирования объектов в Python

Понимание копирования объектов в Python

Содержание
  1. Мои первые трудности с копированием списков
  2. Откровение: Псевдонимы против реальных копий
  3. Правильный подход к копированию в Python
  4. Мелкое копирование
  5. Глубокое копирование
  6. Роль мемо-словаря в Deepcopy

Мои первые трудности с копированием списков

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

def square_nums(nums_list):
    return [num ** 2 for num in nums_list]

original_list = [1, 2, 3, 4, 5]
copied_list = original_list[:]
squared_nums = square_nums(copied_list)

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

Откровение: Псевдонимы против реальных копий

dxxv93von9bfslt22q92.png
dxxv93von9bfslt22q92.png

Правильный подход к копированию в Python

Python предоставляет два способа сделать это:

  1. Неглубокое копирование
  2. Глубокое копирование Обе эти функции доступны в модуле копирования, входящем в стандартную библиотеку Python.

Мелкое копирование

При неглубоком копировании создается новый контейнер/объект, но содержащиеся в нем элементы по-прежнему являются ссылками на исходный контейнер/объект.

В этом можно убедиться, проверив память контейнеров и их отдельных элементов с помощью функции id().

Этот способ копирования лучше всего работает, если у нас неизменяемые элементы.

Это достигается с помощью функции copy.copy() модуля copy.

kuefuxbtd3j4vp3rubyo.png
kuefuxbtd3j4vp3rubyo.png
import copy

original_list = [1, 2, 3]  # Creates a shallow copy
copied_list = copy.copy(original_list)

# Will print True
print(id(original_list[0]) == id(copied_list[0]))

original_list[0] = 0

# Will print False
print(id(original_list[0]) == id(copied_list[0]))

В приведенном выше коде последний оператор печати все равно будет False, потому что целые числа неизменяемы. Поэтому, как только оригинальному_списку[0] будет присвоено новое значение, ссылка обновится до нового целочисленного значения, то есть до 0. Но copied_list[0] продолжает ссылаться на исходный целочисленный объект 1. Такое поведение было бы иным, если бы original_list содержал изменяемые объекты (например, другие списки, словари и т. д.), где изменение изменяемого объекта внутри original_list было бы отражено и в original_list из-за общих ссылок.

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

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

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

import copy

original_list = [1, 2, 3, [2,3,4]]
copied_list = copy.deepcopy(original_list)

# Will print False
print(id(original_list) == id(copied_list))

# Will print True
print(id(original_list[0]) == id(copied_list[0]))

# Will print True
print(id(original_list[1]) == id(copied_list[1]))

# Will print True
print(id(original_list[2]) == id(copied_list[2]))

# Will print False
print(id(original_list[3]) == id(copied_list[3]))

В приведенном выше коде мы видим интересную вещь, а именно: когда объекты неизменяемы, как, например, объекты с индексами 0, 1, 2 в оригинальном_списке, то и объекты в копируемом_списке также ссылаются на тот же объект. Но когда элемент является изменяемым, как список, то в копируемом_списке сохраняется отдельная копия.

5y5lz8bijhixns405apz.png
5y5lz8bijhixns405apz.png

Роль мемо-словаря в Deepcopy

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

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

Реализация в классах, определяемых пользователем

Чтобы реализовать функциональность copy и deepcopy, мы можем использовать магические методы copy() и deepcopy() внутри класса.
Внутренне даже структура данных list является классом, и поэтому мы получаем list.copy() функция.

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