Как работает копирование объектов в Python

автор рубрика ,
Как работает копирование объектов в Python

Копирование объекта в Python подразумевает создание нового объекта, имеющий другой идентификатор (следовательно, это совсем другой объект) и одно и то же содержимое. Копирование позволяет работать с объектом, сохраняя при этом исходный. Тема копирования объектом нуждается в детальном рассмотрении. В этой статье мы рассмотрим два вида копирования, которые используются в Python, — поверхностное и глубокое.

Копирование неизменяемых объектов

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

Если ваш объект неизменяемый, то, чтобы получить экземпляр, который имеет то же самое содержимое, просто используйте присвоение. Пример кода на Python:

string = "Hello, world!"
string_ = string
# Это два разных объекта, несвязанных друг с другом

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

Многие изменяемые объекты могут содержать другие изменяемые объекты. Таким образом, существуют два типа копий:

  • поверхностное (shallow copy),
  • глубокое (deep copy).

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

Разработка и внедрение ML-решений

Код курса
MLOPS
Ближайшая дата курса
25 июля, 2022
Длительность обучения
24 ак.часов
Стоимость обучения
45 000 руб.

Списки и словари имеют метод copy, который возвращает поверхностные копии. Например:

>>> sublist = []
>>> outer_list = [42, 73, sublist]
>>> copy_list = outer_list.copy()

Итак, у нас есть список, который содержит другой список. Затем мы создаем копию с помощью метода copy. Вопрос: полученная копия и исходный список — это разные объекты? Ответ — да:

>>> copy_list is outer_list
False
>>> copy_list[0] = 0
>>> outer_list    # Не повлияло на исходный 
[42, 73, []]

А теперь попробуем изменить содержимое списка sublist и посмотрим, что случится:

>>> sublist.append(999)
>>> copy_list
[0, 73, [999]]
>>> outer_list
[42, 73, [999]]

Изменение содержимого вложенного списка повлекло за собой изменение как copy_list, так и outer_list. Такое поведение очень не очевидно. Казалось бы, эти два объекта различны, но изменение вложенного списка поменял исходный объект и копию. Такие копии называются поверхностными.

На самом деле модификация вложенного списка не изменяет содержимое ни copy_list, ни outer_list. Просто третий элемент обоих объектов связан с другим списком, который в свою очередь может быть модифицирован. В терминах языка Си мы бы говорили о наличие указателя.

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

Как ещё можно создавать поверхностные копии

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

>>> outer_list = [42, 73, []]
>>> shallow_copy = outer_list[::]
>>> outer_list[2].append(999)
>>> shallow_copy
[42, 73, [999]]

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

Или пример создания списка списка:

>>> outer_list = [42, 73, []]
>>> shallow_copy = list(outer_list)
>>> shallow_copy[2].append(999)
>>> outer_list
[42, 73, [999]]

— который приводит к тому же результату. То же самое можно применить и к словарю:

>>> outer_dict = {42: 73, "list": []}
>>> shallow_copy = dict(outer_dict)
>>> outer_dict["list"].append(999)
>>> shallow_copy
{42: 73, 'list': [999]}

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

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

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

def list_deepcopy(l):
    return [
        elem if not isinstance(elem, list) else list_deepcopy(elem)
        for elem in l
    ]

Мы теперь можем воспользоваться этой функцией для копирования списка:

>>> sublist = []
>>> outer_list = [42, 73, sublist]
>>> copy_list = list_deepcopy(outer_list)
>>> sublist.append(73)
>>> copy_list
[42, 73, []]
>>> outer_list
[42, 73, [73]]

Как можно увидеть, изменение списка sublist не повлекло за собой изменение копии.

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

Модуль copy и функция deepcopy

Стандартный Python-модуль copy имеет две нужные нам функции [2]:

  • copy для поверхностного копирования;
  • deepcopy для глубокого.

Функция deepcopy достаточно умна, чтобы справляться с возникающими трудностями, которые могут возникнуть при копировании. Можете даже взглунять на количество проверок в исходном коде.

Если вы пишите со,ственные объекты и хотите указать, какое копирование использовать, то вам нужно реализовать протоколы __copy__ и/или __deepcopy__.

 

О том, как происходит копирование данных большого размера (Big Data) вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и ИТ-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:

Источники
  1. https://mathspp.com/blog/pydonts/pass-by-value-reference-and-assignment
  2. https://docs/3/library/copy.html
Комментировать