Модуль copy — поверхностное и глубокое копирование объектов

Операция присваивания не копирует объект, он лишь создаёт ссылку на объект. Для изменяемых коллекций, или для коллекций, содержащих изменяемые элементы, часто необходима такая копия, чтобы её можно было изменить, не изменяя оригинал. Данный модуль предоставляет общие (поверхностная и глубокая) операции копирования.
copy.copy(x) — возвращает поверхностную копию x.
copy.deepcopy(x) — возвращает полную копию x.
Исключениеcopy.error — возникает, если объект невозможно скопировать.
Разница между поверхностным и глубоким копированием существенна только для составных объектов, содержащих изменяемые объекты (например, список списков, или словарь, в качестве значений которого — списки или словари):
- Поверхностная копия создает новый составной объект, и затем (по мере возможности) вставляет в него ссылки на объекты, находящиеся в оригинале.
- Глубокая копия создает новый составной объект, и затем рекурсивно вставляет в него копии объектов, находящихся в оригинале.
Для операции глубокого копирования часто возникают две проблемы, которых нет у операции поверхностного копирования:
- Рекурсивные объекты (составные объекты, которые явно или неявно содержат ссылки на себя) могут стать причиной рекурсивного цикла;
- Поскольку глубокая копия копирует всё, она может скопировать слишком много, например, административные структуры данных, которые должны быть разделяемы даже между копиями.
Функция deepcopy решает эти проблемы путем:
- Хранения «memo» словаря объектов, скопированных во время текущего прохода копирования;
- Позволения классам, определенным пользователем, переопределять операцию копирования или набор копируемых компонентов.
Этот модуль не копирует типы вроде модулей, классов, функций, методов, следа в стеке, стековых кадров, файлов, сокетов, окон, и подобных типов.
Поверхностная копия изменяемых объектов также может быть создана методом .copy() у списков (начиная с Python 3.3), присваиванием среза (copied_list = original_list[:]), методом .copy() словарей и множеств. Создавать копию неизменяемых объектов (таких, как, например, строк) необязательно (они же неизменяемые).
Для того, чтобы определить собственную реализацию копирования, класс может определить специальные методы __copy__() и __deepcopy__(). Первый вызывается для реализации операции поверхностного копирования; дополнительных аргументов не передается. Второй вызывается для реализации операции глубокого копирования; ему передается один аргумент, словарь memo. Если реализация __deepcopy__() нуждается в создании глубокой копии компонента, то он должен вызвать функцию deepcopy() с компонентом в качестве первого аргумента и словарем memo в качестве второго аргумента.
Для вставки кода на Python в комментарий заключайте его в теги
Копирование объектов в Python
Следует сразу сказать, что оператор присваивания = не создаёт копию объекта. Присваивание создаёт новую переменную, которая дублирует ссылку на исходный объект.
Для примера давайте создадим из старого списка новый список (путем присваивания).
old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 'a']] new_list = old_list new_list[2][2] = 9 print('Old List:', old_list) print('ID of Old List:', id(old_list)) print('New List:', new_list) print('ID of New List:', id(new_list))
Вывод будет следующим:
Old List: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ID of Old List: 140673303268168 New List: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ID of New List: 140673303268168
Мы видим, что у обеих переменных — old_list и new_list — один id (140673303268168). Если внести изменения в любой из этих список, изменятся оба. Но иногда нам нужно создать копию самого объекта, а не копию ссылки на него.
Для копирования объектов в Python используется модуль copy и следующие методы:
- copy() . Копирует объект и возвращает поверхностную копию передаваемого аргумента.
- deepcopy() . Тоже копирует объект, но возвращает полную копию передаваемого аргумента.
Чем отличаются глубокое и поверхностное копирование?
Поверхностное копирование
Поверхностное копирование создает отдельный новый объект или список, но вместо копирования дочерних элементов в новый объект, оно просто копирует ссылки на их адреса памяти. Следовательно, если вы сделаете изменение в исходном объекте, оно будет отражено в скопированном объекте, и наоборот.
Пример поверхностного копирования:
import copy old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] new_list = copy.copy(old_list) print("Old list:", old_list) print("New list:", new_list)
Old list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] New list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Старый список и новый список — разные объекты. Чтобы это доказать, давайте изменим старый список:
import copy old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]] new_list = copy.copy(old_list) old_list.append([4, 4, 4]) print("Old list:", old_list) print("New list:", new_list)
Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4]] New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
В этом примере мы создали поверхностную копию old_list. Новый список (new_list) содержит ссылки на исходные вложенные объекты, хранящиеся в старом списке. Когда мы добавили новый вложенный объект в old_list, это не отразилось на new_list, потому что в последнем не было ссылки на этот новый вложенный объект.
Давайте теперь попробуем изменить один из вложенных объектов, ссылки на которые были скопированы в new_list.
import copy old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]] new_list = copy.copy(old_list) old_list[1][1] = 'AA' print("Old list:", old_list) print("New list:", new_list)
Old list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]] New list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]]
Изменения затронули оба списка, потому что оба они содержат ссылки на один и тот же вложенный объект.
Глубокое (полное) копирование
Глубокая копия создает новую и отдельную копию всего объекта или списка со своим уникальным адресом памяти. Это означает, что любые изменения, внесенные вами в новую копию объекта или списка, не будут отражаться в исходной. Этот процесс происходит следующим образом: сначала создается новый список или объект, а затем рекурсивно копируются все элементы из исходного в новый.
Короче говоря, оба объекта становятся полностью независимы друг от друга. Это похоже на концепцию передачи по значению в таких языках, как C ++, Java и C #.
import copy old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]] new_list = copy.deepcopy(old_list) print("Old list:", old_list) print("New list:", new_list)
Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
Вроде все так же, как и при поверхностном копировании. Но поведение объектов будет отличаться. Давайте попробуем внести изменения в один из вложенных объектов старого списка:
import copy old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]] new_list = copy.deepcopy(old_list) old_list[1][0] = 'BB' print("Old list:", old_list) print("New list:", new_list)
Результат показывает, что изменения отразились только на старом списке:
Old list: [[1, 1, 1], ['BB', 2, 2], [3, 3, 3]] New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
Так происходит потому, что при глубоком копировании копируются не ссылки на вложенные объекты, а сами объекты.
В посте использовались материалы статьи «Глубокое и поверхностное копирование в Python» и код из статьи «Python Shallow Copy and Deep Copy».
Копирование массива в Python. Метод copy для копирования массива. Представление массива
В этой статье мы поговорим, как скопировать массив в Python и чем копирование массива отличается от представления массива. Также рассмотрим поверхностное и глубокое копирование объектов в Python.
Работая с массивами в Python, вы наверняка сталкивались с ситуацией, когда при использовании некоторых функций, возвращающих какой-нибудь результат, с исходным массивом не происходит ничего. Как в примере ниже:

Всё дело в том, что в библиотеке NumPy есть два понятия касаемо массива: копия и представление. И это разные вещи. Посмотрите на код ниже:

На самом деле, всё просто. Когда мы выполняем присваивание b = a, никакого копирования данных на деле не происходит. В памяти компьютера всё так же один массив, а переменные a и b — это даже не переменные, а указатели, указывающие на одни и те же данные. Таким образом, мы обращаемся по разным указателям к одним и тем же данным в памяти и видим в результате одно и то же.
Хорошо, a и b являются указателями, но что тогда с переменной с? На деле, это тоже указатель, ссылающийся на ту же область памяти с данными, правда, представлены эти данные в иной форме. Вот мы и подошли к понятию представления массива, которое существует в NumPy. Действительно, те же данные можно представить в разной форме:

На деле, можно говорить о копировании, если данные в памяти компьютера физически копируются в другое место. Но, как мы уже убедились, операция присваивания копирование не выполняет. Также мы поняли, что те же самые данные могут иметь различные представления.
Присваивание не копирует массивы в Python
Итак, простое присваивание никаких копий массива не выполняет, и это первое, что стоит уяснить. Может показаться, что это всего лишь прихоть создателей Python и NumPy, но всё не так просто. Если бы ситуация обстояла иначе, мы бы работали с памятью напрямую, а отсутствие автоматического копирования во время присваивания — совсем небольшая плата за лёгкость и простоту языка программирования Python.
Давайте приведём ещё парочку примеров на эту тему:

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

Таким образом, у нас есть массив b и массив a, но нельзя забывать о том, что это, по сути, один и тот же массив.
Так как же копировать массивы в Python?
Чтобы сделать полную копию массива в Python, используют метод copy. Он выполняет копирование не только данных массива, но и всех его свойств.

После применения метода copy мы можем говорить о массивах a и b, как о разных массивах и копиях друг друга. Да, эти массивы имеют одинаковые данные, но эти данные не являются одними и теми же. Теперь действительно массив b является копией массива a, и именно это называется копированием массива в терминологии NumPy.
Представление массива
Итак, теперь мы знаем, как сделать копию массива посредством метода copy. Но бывают ситуации, когда нам не нужна копия массива, а нужен тот же массив но с иными размерами. Речь идёт, как вы уже догадались, о другом представлении исходного массива.
Для этих целей в NumPy есть метод ndarray.view() . Он создаёт новый объект массива, просматривающий данные исходного, однако изменение размеров одного массива не приводит к изменению размеров другого.

Обычно, функции которые меняют форму и порядок элементов в Пайтон-массивах возвращают не копию массива, а именно его представление:

Также представлениями массивов в Python являются срезы массивов:

Обратите внимание, что когда мы говорим о том, что массив b является представлением массива a, мы подразумеваем, что вне зависимости от вида и формы массива b он включает в себя те же данные в памяти, что и наш массив a. Таким образом, изменение элементов в одном из массивов приведёт, соответственно, к изменениям в другом.
Глубокое и поверхностное копирование объектов с помощью copy
Как мы уже хорошо уяснили, операция присваивания не приводит к копированию объекта, а лишь создаёт ссылку на этот объект. Но если мы работаем с изменяемыми коллекциями или коллекциями, которые содержат изменяемые элементы, нам может понадобиться такая копия, которую мы сможем изменить, не меняя оригинал. Здесь нам тоже поможет copy, выполняющий как поверхностное, так и глубокое копирование: • copy.copy(a) — возвращает поверхностную копию a; • copy.deepcopy(a) — возвращает полную копию a.
Если же объект скопировать невозможно, возникает исключение copy.error. В принципе, разница между глубоким и поверхностным копированием существенна лишь для составных объектов, которые содержат изменяемые объекты (допустим, список списков). При этом: 1) поверхностная копия позволяет создать новый составной объект, а потом (если это возможно) вставляет в него ссылки на объекты, которые находятся в оригинале; 2) глубокая копия позволяет создать новый составной объект, а потом рекурсивно вставляет в него копии объектов, которые находятся в оригинале.
>>> import copy >>> test_1 = [1, 2, 3, [1, 2, 3]] >>> test_copy = copy.copy(test_1) >>> print(test_1, test_copy) [1, 2, 3, [1, 2, 3]] [1, 2, 3, [1, 2, 3]] >>> test_copy[3].append(4) >>> print(test_1, test_copy) [1, 2, 3, [1, 2, 3, 4]] [1, 2, 3, [1, 2, 3, 4]] >>> test_1 = [1, 2, 3, [1, 2, 3]] >>> test_deepcopy = copy.deepcopy(test_1) >>> test_deepcopy[3].append(4) >>> print(test_1, test_deepcopy) [1, 2, 3, [1, 2, 3]] [1, 2, 3, [1, 2, 3, 4]]При выполнении глубокого копирования возможны проблемы (их нет у поверхностного копирования): — рекурсивные объекты могут привести к рекурсивному циклу; — т. к. глубокая копия копирует всё, она способна скопировать слишком много, к примеру, административные структуры данных.
Однако в случае возникновения проблем нам поможет функция deepcopy, которая устраняет эти сложности: — посредством хранения "memo" словаря объектов; — позволяя классам, которые определяет пользователь, переопределять операцию копирования либо набор копируемых компонентов.
>>> r = [1, 2, 3] >>> r.append(r) >>> print(r) [1, 2, 3, [. ]] >>> p = copy.deepcopy(r) >>> print(p) [1, 2, 3, [. ]]В результате, не копируются типы вроде классов, функций, модулей, методов, стековых кадров, окон, сокетов и т. п.
Что же, теперь, надеемся, вы получили представление о копировании массивов и объектов в Python. Если хотите знать больше, к вашим услугам специализированный курс для продвинутых разработчиков:
Как можно изменить массив "A", чтобы не изменилась его копия "B"?
Чтобы у Вас не менялась переменная B , сделайте следующее:
A = B.copy()Отслеживать
25.4k 4 4 золотых знака 20 20 серебряных знаков 36 36 бронзовых знаков
ответ дан 7 дек 2022 в 6:19
191 8 8 бронзовых знаковЕще есть такой способ:
A = B[:]Я всегда использовал это. Про copy() слышал, но не пользовался.
Отслеживать
ответ дан 7 дек 2022 в 9:22
2,744 4 4 золотых знака 9 9 серебряных знаков 26 26 бронзовых знаковДа, по сути это тоже самое. Но всё же путь питона это "явное - лучше, чем не явное", поэтому лично на мой взгляд .copy() как явная копия - лучше. )
7 дек 2022 в 9:58
Ещё один способ скопировать список, с использованием list :
A = list(B)Отслеживать
ответ дан 2 апр 2023 в 10:21
3,504 6 6 золотых знаков 12 12 серебряных знаков 31 31 бронзовый знак
Минус-то за что? за eval что ли?
25 дек 2023 в 13:26
За эвал. Не нужно так делать ни в каком коде, ни в "настоящем", ни в "не настоящем".
26 дек 2023 в 6:50@insolor, написал, чтобы нигде не использовали. А вообще-то выглядит это, конечно, ужасно, но уезвимостей-то тут нет (или я их не вижу): это всё-таки не eval(input()) .
26 дек 2023 в 9:47
Есть простое правило: не использовать eval, даже если очень хочется, даже если источник строки безопасен. Для вычисления математических выражений есть сторонние безопасные "калькуляторы". Для парсинга литералов в строке есть стандартный безопасный ast.literal_eval , но что eval , что literal_eval - это явный оверкилл для такой простой задачи, как копирование списка. Это больше похоже на забивание гвоздей микроскопом.
26 дек 2023 в 9:51
Во втором Питоне input() был реализован как eval(raw_input()) . На вход пользователь мог подавать любые выражения. Сама идея заимствована из раннего BASIC, там тоже ввод пользователя интерпретировался.