Копирование объектов — одна из фундаментальных концепций в Python, непонимание которой часто приводит к трудноуловимым ошибкам. Когда мы изменяем один объект, а неожиданно меняется и другой, скорее всего, проблема кроется в неправильном копировании. Давайте разберемся в этом раз и навсегда.
🔎 Почему вообще важно копирование объектов?
В Python переменные — это не «коробки» с данными, а скорее «ярлыки» или ссылки, указывающие на объекты в памяти. Когда вы выполняете простое присваивание, например:
list_a = [1, 2, 3]
list_b = list_a
Вы не создаете новый список. Вы просто создаете еще одну ссылку (list_b), которая указывает на тот же самый объект списка.
👉 Любое изменение list_a немедленно отразится в list_b (и наоборот), потому что это один и тот же объект.
Аналогия: Представьте, что у вас есть документ в Google Docs. Если вы отправите другу ссылку на него, он будет редактировать ваш оригинальный документ. Это и есть присваивание. А вот если вы выберете «Файл» → «Создать копию», появится полностью независимый документ. Именно для создания таких независимых копий в Python существует модуль copy.
⚙️ Два типа копирования
Модуль copy предоставляет две ключевые функции:
copy.copy()— создает поверхностную копию.copy.deepcopy()— создает глубокую копию.
Основное различие между ними заключается в том, как они обрабатывают вложенные объекты (например, списки внутри списков или словари внутри словарей).
🟦 Поверхностное копирование (shallow copy)
copy.copy() создает новый объект-контейнер, но вместо копирования вложенных объектов он просто вставляет в новый контейнер ссылки на них.
import copy
original_list = [1, 2, [3, 4]]
shallow_copy = copy.copy(original_list)
# Изменим вложенный список в оригинале
original_list[2][0] = 99
print(f"Оригинал: {original_list}") # Вывод: Оригинал: [1, 2, [99, 4]]
print(f"Поверхностная копия: {shallow_copy}") # Вывод: Поверхностная копия: [1, 2, [99, 4]]
Как видите, изменение вложенного списка затронуло и оригинал, и копию, потому что этот вложенный список у них общий.
ASCII-диаграмма:
original_list → [ 1, 2, ───► list_inner ]
shallow_copy → [ 1, 2, ───► list_inner ]
list_inner → [ 99, 4 ]
🟩 Глубокое копирование (deep copy)
copy.deepcopy() решает проблему общих ссылок. Эта функция рекурсивно обходит весь объект и создает полные, независимые копии всего, что встречает на своем пути, включая все вложенные объекты.
import copy
original_list = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original_list)
# Изменим вложенный список в оригинале
original_list[2][0] = 99
print(f"Оригинал: {original_list}") # Вывод: Оригинал: [1, 2, [99, 4]]
print(f"Глубокая копия: {deep_copy}") # Вывод: Глубокая копия: [1, 2, [3, 4]]
Теперь deep_copy полностью независима.
ASCII-диаграмма:
original_list → [ 1, 2, ───► list_a ]
deep_copy → [ 1, 2, ───► list_b ]
list_a → [ 99, 4 ]
list_b → [ 3, 4 ]
📑 Шпаргалка: copy.copy() vs copy.deepcopy()
| Тип объекта | Пример | copy.copy() (поверхностная) | copy.deepcopy() (глубокая) |
|---|---|---|---|
| list (список) | [1, 2, [3, 4]] | Новый список, но вложенный [3, 4] общий | Полностью независимая копия, вложенный список тоже копируется |
| dict (словарь) | {"a": 1, "b": {"c": 2}} | Новый словарь, но вложенный {"c": 2} общий | Независимый словарь и все вложенные словари |
| set (множество) | {1, 2, (3, 4)} | Новое множество, вложенные изменяемые элементы общие | Новое множество, все вложенные элементы рекурсивно копируются |
| tuple (кортеж) | (1, 2, [3, 4]) | Возвращает тот же кортеж, но вложенный список общий | Новый кортеж с копией вложенного списка |
| str, int, float, bool | "hi", 42, 3.14, True | Возвращает тот же объект (они неизменяемы, копировать нет смысла) | То же самое |
| custom class | Node(1, Node(2)) | Новый объект верхнего уровня, вложенные объекты общие | Полностью независимая копия объекта и всех вложенных объектов |
object with __slots__ | Класс с __slots__ | Копируется только объект верхнего уровня | Копируются все вложенные объекты, если они поддерживают копирование |
| файл / сокет / поток | open("file.txt") | Ошибка (некопируемый объект) | Ошибка (некопируемый объект) |
🧪 Живые примеры к таблице
📌 list
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # [1, 2, [99, 4]]
print(f"deepcopy: {c}") # [1, 2, [3, 4]]
📌 dict
import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}") # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}") # {'a': 1, 'b': {'c': 2}}
📌 set
import copy
a = {1, 2, (3, 4)} # Множества могут содержать только неизменяемые типы
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (но это два независимых объекта в памяти)
📌 tuple
import copy
a = (1, 2, [3, 4]) # Кортеж неизменяем, но его элемент-список — да
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # (1, 2, [99, 4])
print(f"deepcopy: {c}") # (1, 2, [3, 4])
📌 Custom class
import copy
class Node:
def __init__(self, value, child=None):
self.value = value
self.child = child
a = Node(1, Node(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.child.value = 99
print(f"copy: {b.child.value}") # 99
print(f"deepcopy: {c.child.value}") # 2```
---
#### **📌 Кастомизация поведения: `__copy__` и `__deepcopy__`**
Вы можете управлять тем, как копируются ваши собственные объекты, определив в их классе специальные методы.
python
import copy
class Custom:
def init(self, x):
self.x = x
def copy(self):
print(«Вызван copy!»)
# Возвращаем новый экземпляр, но с той же ссылкой на внутренние данные
return Custom(self.x)
def deepcopy(self, memo):
# memo — словарь для отслеживания уже скопированных объектов (защита от рекурсии)
print(«Вызван deepcopy!»)
# Создаем полностью новую копию, включая вложенные данные
return Custom(copy.deepcopy(self.x, memo))
obj = Custom([1, 2])
c1 = copy.copy(obj) # Вывод: Вызван copy!
c2 = copy.deepcopy(obj) # Вывод: Вызван deepcopy!
«`
⚡ Производительность и подводные камни
- Скорость:
copy.copy()работает значительно быстрее, так как не обходит рекурсивно всю структуру данных.copy.deepcopy()может быть очень медленным на больших и глубоко вложенных объектах. - Некопируемые объекты: Системные ресурсы, такие как открытые файлы, сокеты или потоки, скопировать нельзя. Попытка сделать это вызовет ошибку.
- Циклические ссылки:
deepcopyдостаточно умен, чтобы обрабатывать циклические ссылки (когда объектAссылается наB, аB— обратно наA), не уходя в бесконечную рекурсию.
Когда что использовать: главные правила
- Используйте поверхностное копирование (
copy.copy()), если ваш объект содержит только неизменяемые данные или если вы сознательно хотите, чтобы копия и оригинал разделяли общие вложенные объекты. Это быстро и эффективно. - Используйте глубокое копирование (
copy.deepcopy()), когда вам нужна полная, стопроцентная независимость копии от оригинала, особенно при работе со сложными структурами данных (списками, словарями, экземплярами классов). - Для неизменяемых объектов (числа, строки, кортежи без изменяемых элементов) копирование, как правило, не имеет смысла — Python и так оптимизирует их использование.
- Помните о возможности кастомизации через
__copy__и__deepcopy__для полного контроля над процессом.