Модуль `copy` в Python

Модуль `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-канал.

YouTube Video