Модуль `copy` в Python
В этой статье объясняется модуль copy в Python.
Сосредоточившись на различиях между поверхностными и глубокими копиями, мы даем ясное объяснение — от базовых механизмов дублирования объектов до применения в пользовательских классах — с практическими примерами.
YouTube Video
Модуль copy в Python
Модуль Python copy — это стандартный инструмент для работы с дублированием (копированием) объектов. Цель — понять различия между поверхностным и глубоким копированием и уметь управлять поведением копирования для пользовательских объектов.
Основы: что такое поверхностная копия?
Здесь мы демонстрируем поведение поверхностных копий для изменяемых объектов, таких как списки и словари. Поверхностная копия дублирует только объект верхнего уровня и разделяет внутренние ссылки.
1# Example: shallow copy with lists and dicts
2import copy
3
4# A nested list
5original = [1, [2, 3], 4]
6shallow = copy.copy(original)
7
8# Mutate nested element
9shallow[1].append(99)
10
11print("original:", original) # nested change visible in original
12print("shallow: ", shallow)В этом коде список верхнего уровня дублируется, но внутренние подсписки разделяются по ссылке, поэтому shallow[1].append(99) также отражается в original. Это типичное поведение поверхностной копии.
Что такое глубокая копия?
Глубокая копия рекурсивно дублирует сам объект и все объекты, на которые он ссылается, создавая независимый объект. Используйте ее, когда нужно безопасно дублировать сложные вложенные структуры.
1# Example: deep copy with nested structures
2import copy
3
4original = [1, [2, 3], {'a': [4, 5]}]
5deep = copy.deepcopy(original)
6
7# Mutate nested element of the deep copy
8deep[1].append(100)
9deep[2]['a'].append(200)
10
11print("original:", original) # original remains unchanged
12print("deep: ", deep)В этом примере изменения в deep не влияют на original. deepcopy копирует весь граф объектов, создавая независимую реплику.
Как решить, какое копирование использовать
Выбор типа копии зависит от структуры объекта и ваших целей. Если вы не хотите, чтобы изменения внутренних изменяемых элементов затрагивали исходный объект, используйте deepcopy. Потому что он рекурсивно дублирует весь объект, создавая полностью независимую копию.
С другой стороны, если достаточно дублировать только объект верхнего уровня и для вас важны скорость и экономия памяти, более уместен copy (поверхностная копия).
- Хотите избежать изменений исходного объекта при модификации внутренних изменяемых элементов → используйте
deepcopy. - Достаточно дублирования только верхнего уровня, и вы хотите дать приоритет производительности (скорость/память) → используйте
copy(поверхностная копия).
Работа со встроенными и неизменяемыми объектами
Неизменяемые объекты (например, int, str и кортежи с неизменяемым содержимым) обычно не требуется копировать. copy.copy при необходимости может вернуть тот же самый объект.
1# Example: copying immutables
2import copy
3
4a = 42
5b = copy.copy(a)
6print(a is b) # True for small ints (implementation detail)
7
8s = "hello"
9t = copy.deepcopy(s)
10print(s is t) # True (strings are immutable)Поскольку копирование неизменяемых объектов дает мало выигрыша по эффективности, один и тот же объект может переиспользоваться. Об этом редко приходится беспокоиться при проектировании приложений.
Пользовательские классы: определение __copy__ и __deepcopy__
Если поведение копирования по умолчанию не соответствует ожиданиям для вашего класса, вы можете предоставить собственную логику копирования. Если вы определите __copy__ и __deepcopy__, то copy.copy и copy.deepcopy будут их использовать.
1# Example: customizing copy behavior
2import copy
3
4class Node:
5 def __init__(self, value, children=None):
6 self.value = value
7 self.children = children or []
8
9 def __repr__(self):
10 return f"Node({self.value!r}, children={self.children!r})"
11
12 def __copy__(self):
13 # Shallow copy: create new Node, but reuse children list reference
14 new = self.__class__(self.value, self.children)
15 return new
16
17 def __deepcopy__(self, memo):
18 # Deep copy: copy value and deepcopy each child
19 new_children = [copy.deepcopy(child, memo) for child in self.children]
20 new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21 memo[id(self)] = new
22 return new- Реализовав методы
__copy__и__deepcopy__, вы можете гибко управлять специфичным для класса поведением копирования. Например, можно обрабатывать случаи, когда некоторые атрибуты должны ссылаться на (разделять) один и тот же объект, тогда как другие атрибуты должны дублироваться как полностью новые объекты. - Аргумент
memo, передаваемый в__deepcopy__, — это словарь, который фиксирует объекты, уже обработанные в ходе рекурсивного копирования, и предотвращает бесконечные циклы, вызванные циклическими ссылками.
1# Build tree
2root_node = Node('root', [Node('child1'), Node('child2')])
3
4shallow_copy = copy.copy(root_node) # shallow copy
5deep_copy = copy.deepcopy(root_node) # deep copy
6
7# Modify children
8# affects both root_node and shallow_copy
9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)- В этом коде переменная
shallow_copyсоздаётся посредством 'поверхностного копирования', поэтому дублируется только верхний уровень объекта, а объекты, на которые он ссылается внутри (в данном случае списокchildren), являются общими с исходнымroot_node. В результате при добавлении нового узла вshallow_copyобновляется общий списокchildren, и изменение отражается также в содержимомroot_node. - С другой стороны, переменная
deep_copyсоздаётся посредством 'глубокого копирования', поэтому внутренняя структура рекурсивно дублируется, формируя дерево, полностью независимое отroot_node. Поэтому, даже если вы добавите новый узел вdeep_copy, это не повлияет наroot_node.
Циклические ссылки (рекурсивные объекты) и важность memo
Когда сложный граф объектов ссылается сам на себя (циклические ссылки), deepcopy использует словарь memo для отслеживания уже скопированных объектов и предотвращения бесконечных циклов.
1# Example: recursive list and deepcopy memo demonstration
2import copy
3
4a = []
5b = [a]
6a.append(b) # a -> [b], b -> [a] (cycle)
7
8# deepcopy can handle cycles
9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))- Внутри
deepcopyиспользуетmemoдля обращения к уже дублированным объектам, что позволяет безопасно копировать даже при наличии циклов. Похожий механизм нужен и при ручной реализации рекурсии.
copyreg и пользовательское копирование в стиле сериализации (продвинутое)
Если вы хотите зарегистрировать особое поведение копирования в координации с библиотеками, используйте стандартный модуль copyreg для регистрации того, как объекты конструируются/воссоздаются. Это полезно для сложных объектов и типов из C-расширений.
1# Example: using copyreg to customize pickling/copy behavior (brief example)
2import copy
3import copyreg
4
5class Wrapper:
6 def __init__(self, data):
7 self.data = data
8
9def reduce_wrapper(obj):
10 # Return callable and args so object can be reconstructed
11 return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)- С помощью
copyregможно задать правила реконструкции, влияющие как наpickle, так и наdeepcopy. Это продвинутый API, и в большинстве случаев достаточно__deepcopy__.
Практические оговорки и подводные камни
Есть несколько важных моментов, чтобы обеспечить корректное поведение при использовании модуля copy. Ниже мы объясняем распространенные ловушки, с которыми можно столкнуться в разработке, и как их избежать.
- Производительность
deepcopyможет потреблять значительные ресурсы памяти и CPU, поэтому тщательно оценивайте, действительно ли он необходим. - Обработка элементов, которые нужно разделять
Если вы хотите, чтобы некоторые атрибуты, например большие кэши, оставались общими, намеренно избегайте копирования их ссылок внутри
__deepcopy__. - Неизменяемое внутреннее состояние Если вы храните внутри неизменяемые данные, копирование может быть излишним.
- Потоки и внешние ресурсы Ресурсы, которые нельзя копировать (например, сокеты и файловые дескрипторы), либо бессмысленно копировать, либо они вызовут ошибки, поэтому по замыслу их копирование следует избегать.
Практический пример: шаблон безопасного обновления словарей
При обновлении сложного объекта конфигурации этот пример использует deepcopy для защиты исходных настроек.
1# Example: safely update a nested configuration using deepcopy
2import copy
3
4default_config = {
5 "db": {"host": "localhost", "ports": [5432]},
6 "features": {"use_cache": True, "cache_sizes": [128, 256]},
7}
8
9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)С помощью deepcopy можно безопасно создавать производные конфигурации, не рискуя повредить настройки по умолчанию. Это особенно полезно для вложенных изменяемых структур, таких как конфигурации.
Лучшие практики
Чтобы безопасно и эффективно использовать модуль copy, важно помнить о следующих практических рекомендациях.
- Выбирайте между поверхностной и глубокой копией исходя из того, будут ли изменения и каков их охват.
- Реализуйте
__copy__и__deepcopy__в пользовательских классах, чтобы получить ожидаемое поведение. - Поскольку глубокие копии дороги, по возможности снижайте потребность в копировании за счет проектных решений. Рассмотрите неизменяемость и явные методы клонирования среди прочих приемов.
- При работе с циклическими ссылками либо используйте
deepcopy, либо вручную обеспечьте механизм, подобныйmemo. - Проектируйте так, чтобы внешние ресурсы, такие как файловые дескрипторы и потоки, не копировались.
Заключение
Модуль copy — стандартный инструмент для дублирования объектов в Python. Правильно понимая различия между поверхностными и глубокими копиями и при необходимости реализуя пользовательское поведение копирования, вы сможете выполнять дублирование безопасно и предсказуемо. Прояснив на этапе проектирования, действительно ли нужно копирование и что должно быть общим, вы сможете избежать лишних ошибок и проблем с производительностью.
Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.