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

Давайте сделаем шаг назад и обратимся к нашим структурам данных. К нашим, в буквальном смысле, кучам данных (и к стекам тоже, конечно).

Что происходит, когда мы присваиваем значение переменной? И что происходит, когда мы передаем эту переменную в качестве параметра?

Как программа узнает, что именно мы хотим ей сказать?

Чтобы понять, как думает компьютер, давайте объявим следующую переменную:

my_variable = ['r', 'a', 'n', 'd', 'o', 'm']

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

В данном примере программа сохранит вашу строку следующим образом:

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

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

Но зачем все это? Зачем мы оперируем разными структурами данных?

Ответ очень прост. Все дело в скорости.

Стек, в сравнении с кучей, гораздо быстрей. Потому что ссылку передать намного проще, чем перемещать сами данные. Имеется ввиду, что не нужно нагружать программу лишними задачами, которые отвлекали бы ее от основной.

Надо заметить, что не все типы данных хранятся подобным образом. Статические типы данных, которыми являются значения логических, целочисленных, с плавающей запятой и символьных переменных, хранятся непосредственно в самом стеке. Таким образом, для хранения числа 458.98 куча не используется, так как размер памяти, необходимый для содержания данного числа, уже заранее известен программе.

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

А вот составные типы данных, состоящие из двух и более переменных статического типа, хранятся так, как показано на рисунке.

К составным типам данных, например, относятся:

  • строки (состоящие из переменных символьного типа)
  • массивы
  • кортежи

Возвращаемся к копированию

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

Давайте присвоим переменной некое значение, а затем присвоим значение первой переменной во вторую:

my_variable = ['r', 'a', 'n', 'd', 'o', 'm']

my_other_variable = my_variable

Как бы мы ни выводили значения этих переменных, результатом будет строка «random». Но мы задублировали только стек, но не кучу:

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

А что будет, если мы захотим изменить одну из этих переменных?

my_other_variable  += ['r', 'a', 'm', 'b', 'l', 'i', 'n','g'] 

print(f"My second variable: {my_other_variable}")
print(f"My variable: {my_variable}")

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

['r', 'a', 'n', 'd', 'o', 'm', 'r', 'a', 'm', 'b', 'l', 'i', 'n', 'g']

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

import copy

my_variable = ['r', 'a', 'n', 'd', 'o', 'm']
my_other_variable = copy.deepcopy(my_variable)

После этого вы можете делать с вашими переменными все, что угодно. Хоть отправляйте их на Луну.

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

Прокрутить вверх