Мелкое и глубокое копирование объектов в Python
Иногда, при программировании на Python у нас возникает потребность в дублировании (создании копии) того или иного объекта данных (списка, словаря, класса и т.д) так, чтобы последующие изменения нашего первоначального объекта никак не отражались бы на предварительно созданном его дубли. К сожалению, стандартные операторы присваивания Python, такие как =
и :=
не справляются с этой задачей, а лишь только привязывают новые имена к уже имеющимся объектам в ОЗУ. По сути, использование стандартных операторов присваивания для вышеназванных объектов данных может иметь смысл лишь тогда, когда эти объекты являются неизменяемыми. Но если нам требуются "настоящие копии" или "клоны" объектов для последующих их изменений вне зависимости от оригиналов, то придется прибегнуть к дополнительному инструментарию Python, о котором и пойдет речь ниже в этой статье.
Примечание. Все примеры кода в данной статье изложены на основе синтаксиса и возможностей Python 3. Но, так как разница между Python 2 и 3 относительно копирования объектов весьма незначительна, то приведенный ниже код в основном может быть выполнен и на старой версии языка. Те же редкие особенности в кодинге для Python 2 в статье будут оговариваться отдельно. Для начала, следует отметить, что некоторые стандартные коллекции в Python, такие как: списки, словари и наборы предусматривают наличие собственных встроенных функций, которые как раз и позволяют осуществлять реальное копирование своих объектов данных:
new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)
Но, все вышеприведенные функции относятся лишь к стандартным коллекциям своего типа и никак не могут быть применены к пользовательским объектам данных. Кроме того, все эти функции реально создают настоящие клоны лишь для поверхностного уровня структур в присущих им коллекциях. Дело в том, что для многоуровневых – вложенных объектов данных (тех же списков, словарей и наборов) существует важное различие между поверхностным (мелким) и глубоким копированием:
- Мелкое копированиеозначает создание (дублирование) элементов копируемого объекта данных (коллекции) лишь для 1-го (самого верхнего) уровня вложенности. На все же остальные элементы, которые в оригинале размещаются на более глубоких уровнях, копируемый объект может только ссылаться. Следовательно, все элементы в копируемом объекте, начиная со 2-го уровня и более, при изменении своих аналогов в оригинале будут также автоматически изменяться.
- Глубокое копированиепредставляет собой рекурсивное копирование, при котором в скопированный объект данных из оригинала поэтапно переносятся копии элементов со всех уровней вложенности, найденных в оригинале. В результате такого копирования из оригинального объекта данных создается абсолютно обособленный клон, каждый элемент которого на любом уровне своей вложенности может быть изменен без каких-либо последствий.
Итак, давайте более подробно рассмотрим разницу между мелким и глубоким копированием.
Создание мелких копий
Изучение подробностей мелкого копирования мы начнем с создания нового вложенного (двухуровневого) списка, из которого затем сделаем дубликат путем мелкого его копирования с помощью встроенной для списков функции list():
>>> s1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> s2 = list(s1) # Создание мелкой копии
В результате мы получили формально независимый список s2
, содержимое которого абсолютно идентично содержимому списка s1
:
>>> print(s1)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> print(s2)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Для того, чтобы убедиться, что список s2
действительно независим от оригинала, на основе которого он был создан, давайте попробуем добавить в этот оригинал (список s1
) новый подсписок, а затем проверим, не повлияло ли это изменение на наш дубликат (s2
):
>>> s1.append(['новый подсписок'])
>>> print(s1)
[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['новый подсписок']]
>>> print(s2)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Как видим, изменение оригинального списка на первом (поверхностном) уровне вообще не повлияло на содержимое нашего списка-дубликата (s2
).
Однако, поскольку в дубликате s2
мы создали лишь мелкую копию списка s1
, то этот дубликат на своем 2-м уровне вложенности содержит не свои реальные элементы, а лишь только ссылки, указывающие на соответствующие элементы, которые фактически размещены в списке s1
.
Следовательно, когда мы попробуем изменить один из элементов 2-го уровня в списке s1
, то это же изменение автоматически будет продублировано и в списке s2
, так как в обоих этих списках совместно используются одни и те же элементы из 2-го уровня вложенности:
>>> s1[1][0] = 'X'
>>> print(s1)
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], ['новый подсписок']]
>>> print(s2)
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
В примере выше мы лишь внесли изменения во 2-й подписок нашего исходного списка s1
. Однако, в конечном итоге, вторые подсписки (подписки с индексом 1) были изменены, как в списке s1
, так и в списке s2
. Это объясняется тем, что дубликат s2
был создан нами из исходного списка именно на основе мелкого (поверхностного) копирования.
Если бы при дублировании s1
в s2
мы использовали глубокое копирование, то оба наши списка были бы полностью независимыми. В вышеописанной ситуации, как раз и проявляется разница между мелким и глубоким копированием объектов данных.
На данный момент мы уже знаем, как создавать копии поверхностных (первых) уровней вложенности для некоторых стандартных объектов данных (коллекций). Мы знаем также разницу между мелким и глубоким копированием. Но, мы все еще не знаем ответов на следующие вопросы:
- Как с помощью глубокого копирования создавать дубликаты (копии) из стандартных объектов данных (коллекций)?
- Как воспользоваться мелким или глубоким копированием для создания копий сложно структурированных (комбинированных) объектов, включая пользовательские классы?
Решение всех этих вопросов состоит в применении модуля copy, который размещается в стандартной библиотеке Python и предоставляет простой интерфейс в области мелкого и глубокого копирования для создания копий разнообразных объектов Python.
Создание глубоких копий
Повторим пример с копированием списка из предыдущего раздела. Но, на этот раз попробуем создать из списка s1
глубокую его копию при помощи функции deepcopy(), размещенной в Python модуле copy:
>>> import copy
>>> s1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> s3 = copy.deepcopy(s1)
При проверки списка s1
и его повторного клона s3
, только что созданного нами с помощью copy.deepcopy(), мы увидим, что оба эти списка, как и в предыдущем примере, выглядят абсолютно идентично:
>>> print(s1)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> print(s3)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Однако если в оригинальном списке s1
мы попробуем изменить какой-либо элемент на его 2-м уровне, например, 1-й элемент во втором его подсписке, то увидим, что данное изменение абсолютно никак не повлияет на наш дубликат s3
, сделанный при помощи глубокого копирования.
Оба наши списка, как оригинал, так и копия сейчас являются полностью независимыми вследствие того, что для s1
на этот раз были рекурсивно клонированы абсолютно все его элементы на всех существующих у него уровнях вложенности:
>>> s1[1][0] = 'X'
>>> print(s1)
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
>>> print(s3)
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Теперь настало время вам самим попробовать попрактиковаться в создании мелких и глубоких копий объектов данных на интерпретаторе Python. Во всяком случае, такая практика некогда не будет для вас лишней, так как позволит намного быстрее и глубже зафиксировать в вашей памяти все основные приемы и способы копирования самых разнообразных объектов.
Кстати, в рамках планируемой практики с целью создания мелких копий объектов очень могла бы пригодиться функция copy() из одноименного модуля Python, которая осуществляет поверхностное копирование сложно структурированных (комбинированных) объектов, включая пользовательские классы. Конечно же, функция copy.copy() позволяет создавать поверхностные копии и для таких стандартных коллекций, как: списки, словари и наборы. Однако более природным и соответствующим Python стилю при таком поверхностном (мелком) копировании объектов этих стандартных коллекций, является все же использование их встроенных функций: list, dict и set.
Копирование сложно структурированных объектов Python
Единственное, в чем мы еще не разобрались, так это в том, как же все-таки при помощи мелкого и глубокого копирования нам следует создавать копии сложно структурированных (комбинированных) объектов данных в Python. Попробуем же сейчас разобраться с этой ситуацией посредством использования все того же, уже знакомого нам Python модуля copy. Применение данного модуля для копирования наших сложно структурированных объектов вполне очевидно, поскольку именно его функции copy.copy() и copy.deepcopy() изначально предназначены для дублирования самых разнообразных объектов (включая пользовательские классы) любой сложности и структуры.
Наилучшим способом освоить все тонкости копирования наших сложно структурированных объектов и пользовательских классов, как всегда, является практика. Поэтому, начнем наш путь по освоению данной темы с определения простого класса, принимающего и возвращающего координаты всего лишь одной точки в двумерном пространстве:
class Point_coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Координата Х = {self.x!r}, а координата Y = {self.y!r}'
Класс Point_coordinate по факту кроме инициализатора содержит всего лишь единственный метод __repr__(), возвращающий те же свойства (координаты точки) для своих экземпляров, которые мы должны будем им задавать при инициализации.
Примечание. В приведенном выше примере для вывода результата в методе __repr__() используется новый стиль форматирования строк -(f-строка), который может применяться начиная с Python 3.6. Поэтому в версиях Python 2 и Python 3 ниже 3.6, нам придется использовать иной синтаксис, подобный нижеследующему примеру:
def __repr__(self):
return 'координата Х = %r, координата Y = %r' % (self.x, self.y)
Далее, используя инициализатор класса Point_coordinate
мы создадим из него экземпляр object_a
, который затем (поверхностно) скопируем в object_b
, используя функцию copy() одноименного модуля:
>>> object_a = Point_coordinate(23, 42)
>>> object_b = copy.copy(object_a)
Если мы проверим содержимое исходного экземпляра object_a
и его (поверхностного) клона object_b
, то увидим, что оба эти экземпляры класса Point_coordinate
абсолютно идентичны:
>>> print(object_a)
координата Х = 23, координата Y = 42
>>> print(object_b)
координата Х = 23, координата Y = 42
>>> print(object_a is object_b)
False
Особенность нашего класса Point_coordinate
и его экземпляров заключается в том, что они для своих свойств (координат точки) используют неизменяемые int значения. Поэтому, в приложении к данной ситуации, мелкое или глубокое копирование объектов рассматриваемого класса возымеет абсолютно равнозначный результат.
Но, давайте перейдем к более сложному примеру, в котором мы создадим еще один класс Quadrilateral
с координатами диагонали для последующего отображения двумерных прямоугольников. При этом верхнюю левую и нижнюю правую точки для диагонали в инициализаторе создаваемого нами класса будем указывать с помощью координат, заданных в экземплярах класса Point_coordinate
:
class Quadrilateral:
def __init__(self, left_top, right_lower):
self.left_top = left_top
self.right_lower = right_lower
def __repr__(self):
return (f'Левая верхняя точка [{self.left_top!r}]\n'
f'Правая нижняя точка [{self.right_lower!r}]')
И, снова с помощью инициализатора Quadrilateral
создадим экземпляр этого класса rect1
, а затем посредством мелкого копирования с помощью функции copy.copy() продублируем имеющейся экземпляр в объект rect2
:
rect1 = Quadrilateral(Point_coordinate (0, 1), Point_coordinate (5, 6))
rect2 = copy.copy(rect1)
Если после этого мы сравним оригинальный экземпляр rect1
с его дублем rect2
, то помимо должной оценки возможностей по переопределению метода __repr__(), убедимся в том, что невзирая на поверхностное копирования, два имеющиеся у нас экземпляра пока что совершенно одинаковы:
>>> print(rect1)
Левая верхняя точка [координата Х = 0, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
>>> print(rect2)
Левая верхняя точка [координата Х = 0, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
>>> print(rect1 is rect2)
False
Но что, если теперь мы попробуем прямо в оригинальном экземпляре класса Quadrilateral
изменить координату Х
в верхней левой точке диагонали? Другими словами, что будет, если мы изменим свойства подобъекта (координат точки), который был вложен в оригинальный объект Quadrilateral
? Давайте убедимся, что все эти действия приведут нас к тому же эффекту, который у нас уже был в примерах выше при изменениях подсписков, полученных в результате мелкого (поверхностного) копирования:
>>> rect1.left_top.x = 999
>>> print(rect1)
Левая верхняя точка [координата Х = 999, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
>>> print(rect2)
Левая верхняя точка [координата Х = 999, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
Казус, который наблюдался у нас при копировании вложенных списков, абсолютно идентичен тому, что происходит сейчас со вложенными объектами в экземплярах классов. Но что, если применив функцию copy.deepcopy() мы из нашего дубликата rect2
на основе глубокого копирования сделаем еще один дубликат rect3, в котором затем изменим все туже координату Х
для верхней левой точки диагонали? Выведем содержание для всех трех созданных нами экземпляров класса Quadrilateral и увидим какие элементы в этих экземплярах затронуты внесенными нами изменениями:
>>> rect3 = copy.deepcopy(rect2)
>>> rect3.left_top.x = 222
>>> print(rect3)
Левая верхняя точка [координата Х = 222, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
>>> print(rect1)
Левая верхняя точка [координата Х = 999, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
>>> print(rect2)
Левая верхняя точка [координата Х = 999, координата Y = 1]
Правая нижняя точка [координата Х = 5, координата Y = 6]
Вот теперь уже все! На этот раз глубокая копия rect3
является полностью независимой от оригинала rect1
и поверхностной копии rect2
.
В этой статье рассмотрена большая часть вопросов, связанных с тонкостями копирования объектов данных. Но, вполне естественно, что мы все же не смогли здесь осветить полностью все аспекты вышеназванной сферы программирования. Поэтому, в случае возникновения дополнительных вопросов по копированию объектов вы всегда можете обратиться к документации по модулю copy. Например, в этой документации вы сможете найти информацию о том, как управлять процессом копирования объектов с помощью определения для них специальных методов __copy__()
и __deepcopy__()
.
Три вещи, которые нужно запомнить из этой статьи
- Создание поверхностной копии объекта при мелком копировании не приведет к клонированию элементов, размещенных на 2-м и более глубоких уровнях вложенности. Следовательно, получаемая при этом копия объекта с многоуровневой структурой будет зависима от оригинала.
- Глубокое копирование объекта будет рекурсивно клонировать все его элементы, на всех имеющихся у него уровнях вложенности. Клон при таком копировании будет полностью независим от оригинала, но создание копии в этом случае будет происходить медленнее.
- Копирование сложно структурных (комбинированных) объектов, включая пользовательские классы, можно осуществлять с помощью Python модуля copy.
Возможно будет интересно
🏆 Hello, world!
Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.
Как практиковаться в Python?
Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?
Условные конструкции и сопоставление структурных шаблонов
Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов