Shallow и Deep Copy в Python: ключевые различия и сферы применения
#Основы Python #Списки, кортежи, множества #Типы данныхДля кого эта статья:
- Программисты и разработчики, работающие с Python
- Специалисты, занимающиеся отладкой и оптимизацией кода
- Студенты и обучающиеся, желающие углубить знания о копировании объектов в Python
Когда объект неожиданно мутирует и портит данные в другой части кода — вы столкнулись с "призраком копирования" в Python. 72% ошибок в обработке коллекций связаны именно с неправильным выбором между поверхностным и глубоким копированием. Разница между shallow и deep copy может казаться тривиальной для новичков, но именно она становится ключевым фактором стабильности в проектах, оперирующих сложными структурами данных. Пришло время раз и навсегда поставить точку в этом вопросе и вооружиться правильными инструментами для работы с копированием объектов. 🐍
Основные принципы копирования объектов в Python
В Python всё является объектом, и понимание механизмов копирования критически важно для корректной работы с данными. При присваивании b = a создаётся не новый объект, а новая ссылка на существующий. Это фундаментальный принцип, который отличает Python от многих других языков программирования.
Существует три основных способа работы с объектами:
- Присваивание (Assignment) — создание новой ссылки на тот же объект
- Поверхностное копирование (Shallow Copy) — создание нового объекта, но копирование ссылок на вложенные объекты
- Глубокое копирование (Deep Copy) — рекурсивное создание копий всех вложенных объектов
Чтобы понять фундаментальную разницу, рассмотрим простой пример:
import copy
# Оригинальный список с вложенным списком
original = [1, 2, [3, 4]]
# Создаём три варианта "копирования"
assignment = original
shallow = copy.copy(original)
deep = copy.deepcopy(original)
# Изменяем вложенный список
original[2][0] = 'X'
print(original) # [1, 2, ['X', 4]]
print(assignment) # [1, 2, ['X', 4]] – изменилось
print(shallow) # [1, 2, ['X', 4]] – изменилось
print(deep) # [1, 2, [3, 4]] – не изменилось
Вот ключевые принципы, которые нужно запомнить:
| Тип операции | Что происходит с объектами | Что происходит со вложенными объектами |
|---|---|---|
| Assignment (=) | Новая ссылка на тот же объект | Новая ссылка на те же вложенные объекты |
| Shallow Copy | Новый объект | Ссылки на те же вложенные объекты |
| Deep Copy | Новый объект | Новые копии всех вложенных объектов |
Важно понимать, что различие между типами копирования проявляется только при работе с составными объектами, содержащими мутабельные (изменяемые) типы данных, такие как списки, словари и пользовательские классы.
Для неизменяемых (immutable) типов — чисел, строк, кортежей без мутабельного содержимого — разницы между shallow и deep copy нет, поскольку эти объекты не могут изменить своё состояние после создания.
Александр, senior backend-разработчик
Однажды мы получили странный баг в production: данные в кэше периодически менялись сами по себе. Оказалось, что в нашем API мы использовали поверхностное копирование для кэширования результатов запросов к БД. Когда один клиент модифицировал свои данные, это неожиданно влияло на кэшированные данные других пользователей! Ошибка проявлялась редко и только под нагрузкой, поэтому обнаружили её не сразу.
После двух дней дебага мы заменили
copy.copy()наcopy.deepcopy()в ключевых местах, и проблема исчезла. Этот случай научил всю команду внимательнее относиться к копированию объектов — теперь у нас даже есть специальный линтер, который предупреждает о потенциально опасных местах, где критично использовать глубокое копирование.

Shallow Copy: механизм работы и ограничения
Shallow Copy (поверхностное копирование) — это создание нового объекта-контейнера, который содержит ссылки на те же объекты, что и оригинал. Этот механизм экономит память и работает быстрее глубокого копирования, но может приводить к неочевидным побочным эффектам. 🔄
В Python существует несколько способов создания shallow copy:
- Используя модуль
copy:copy.copy(object) - Встроенные методы объектов:
list.copy(),dict.copy() - Срезы для последовательностей:
my_list[:] - Генераторы списков, словарей:
[x for x in original],{k:v for k,v in original.items()} - Конструкторы типов с исходным объектом:
list(original),dict(original)
Рассмотрим конкретный пример поведения shallow copy со сложной структурой данных:
import copy
# Создаём словарь с вложенными структурами
original = {
'name': 'Project X',
'settings': {
'debug': True,
'cache': False
},
'versions': [1, 2, 3]
}
# Создаём shallow copy
shallow = copy.copy(original)
# Изменяем вложенные структуры в оригинале
original['settings']['debug'] = False
original['versions'].append(4)
print(shallow['settings']) # {'debug': False, 'cache': False}
print(shallow['versions']) # [1, 2, 3, 4]
# Но если изменить сам словарь, а не его содержимое:
original['name'] = 'Project Y'
print(shallow['name']) # 'Project X' – не изменилось!
Ограничения shallow copy становятся критичными при работе с:
- Многоуровневыми структурами данных (вложенные списки, словари)
- Сложными объектами с атрибутами-контейнерами
- Циклическими ссылками
- Параллельными процессами/потоками, одновременно изменяющими данные
Поверхностное копирование особенно коварно тем, что может создавать иллюзию изолированности данных, пока вы не начнете модифицировать вложенные объекты.
| Сценарий | Результат при Shallow Copy | Безопасно? |
|---|---|---|
| Модификация самого контейнера | Оригинал не изменяется | ✅ |
| Замена элемента на верхнем уровне | Оригинал не изменяется | ✅ |
| Модификация вложенного мутабельного объекта | Изменения видны в оригинале | ❌ |
| Работа только с примитивными типами | Эквивалентно deep copy | ✅ |
Deep Copy: полное клонирование вложенных структур
Deep Copy (глубокое копирование) — это рекурсивное создание копий всех объектов, содержащихся в исходной структуре данных. Это гарантирует полную изоляцию копии от оригинала, даже на уровне вложенных объектов. 🧬
В Python глубокое копирование реализуется преимущественно через модуль copy:
import copy
# Создаём сложную структуру данных
original = {
'config': {
'params': [1, 2, 3],
'options': {'debug': True}
},
'data': [{'id': 1, 'values': [10, 20]}, {'id': 2, 'values': [30, 40]}]
}
# Создаём глубокую копию
deep_copy = copy.deepcopy(original)
# Модифицируем вложенные объекты в оригинале
original['config']['params'].append(4)
original['data'][0]['values'][0] = 999
# Проверяем, что копия не изменилась
print(deep_copy['config']['params']) # [1, 2, 3]
print(deep_copy['data'][0]['values']) # [10, 20]
Глубокое копирование решает следующие задачи:
- Обеспечивает полную изоляцию данных между копией и оригиналом
- Предотвращает непреднамеренные изменения связанных объектов
- Позволяет безопасно модифицировать копию без влияния на исходные данные
- Корректно обрабатывает циклические ссылки, предотвращая бесконечную рекурсию
При использовании deepcopy нужно знать несколько важных нюансов:
- Обработка циклических ссылок: модуль
copyавтоматически отслеживает уже скопированные объекты, чтобы избежать бесконечной рекурсии. - Кастомизация через специальные методы: классы могут определять своё поведение при копировании с помощью
__copy__и__deepcopy__. - Копирование функций и классов: функциональные объекты обычно копируются по ссылке, а не дублируются.
- Ресурсоёмкость: для больших структур данных глубокое копирование может потреблять значительные ресурсы.
Для иллюстрации работы с циклическими ссылками рассмотрим пример:
import copy
# Создаём структуру с циклической ссылкой
original = {'name': 'cycle'}
original['self'] = original # Ссылка на самого себя!
# Глубокое копирование корректно обработает цикл
copied = copy.deepcopy(original)
# Проверка
print(copied['name']) # 'cycle'
print(copied['self'] is copied) # True – цикл сохранён правильно
print(copied is original) # False – но это разные объекты
Кастомизация поведения при копировании позволяет реализовать сложную логику для пользовательских классов:
import copy
class ComplexData:
def __init__(self, value, reference=None):
self.value = value
self.reference = reference
self._cache = {'expensive_calculation': None}
def __copy__(self):
# Поверхностное копирование без кэша
cls = self.__class__
result = cls(self.value, self.reference)
return result
def __deepcopy__(self, memo):
# Глубокое копирование с оптимизацией
cls = self.__class__
result = cls.__new__(cls)
memo[id(self)] = result
result.value = copy.deepcopy(self.value, memo)
result.reference = copy.deepcopy(self.reference, memo)
result._cache = {} # Кэш не копируем
return result
Елена, Data Scientist
Работая над проектом анализа финансовых данных, я потратила две недели на отладку модели машинного обучения, которая выдавала непредсказуемые результаты. В процессе предобработки я использовала датафреймы pandas, и одна функция неявно модифицировала исходные данные через поверхностную копию.
Проблема усугублялась тем, что ошибка проявлялась только на определённых наборах данных и только при запуске полного пайплайна. Изолированно каждый компонент работал правильно. После того как я заменила
df.copy()(неглубокое копирование в pandas) наcopy.deepcopy(df), всё встало на свои места.Теперь я всегда задаю вопрос: "Что произойдет, если данные изменятся?" и настоятельно рекомендую использовать глубокие копии при работе с аналитическими пайплайнами, особенно когда данные проходят через несколько этапов трансформации.
Производительность и память: сравнение методов копирования
При выборе между типами копирования необходимо учитывать компромисс между безопасностью и эффективностью. Глубокое копирование гарантирует изоляцию данных, но требует больше ресурсов. Поверхностное копирование экономично, но может привести к неожиданным побочным эффектам. 📊
Рассмотрим сравнение производительности на типичных структурах данных:
import copy
import time
import sys
# Подготовка тестовых данных разной сложности
simple_list = list(range(10000))
nested_list = [[i, i+1] for i in range(5000)]
complex_dict = {
f"key_{i}": {
"data": [j for j in range(100)],
"meta": {"created": "today", "modified": [1, 2, 3]}
} for i in range(100)
}
# Функция для измерения времени и памяти
def benchmark(data, copy_func, name):
start = time.time()
result = copy_func(data)
duration = (time.time() – start) * 1000
memory = sys.getsizeof(result)
print(f"{name:15} | Time: {duration:.2f} ms | Memory: {memory} bytes")
return result
# Запускаем тесты
for data, label in [(simple_list, "Simple List"),
(nested_list, "Nested List"),
(complex_dict, "Complex Dict")]:
print(f"\n=== Testing with {label} ===")
ref = benchmark(data, lambda x: x, "Reference")
shallow = benchmark(data, copy.copy, "Shallow Copy")
deep = benchmark(data, copy.deepcopy, "Deep Copy")
Ключевые выводы о производительности:
| Параметр | Присваивание | Shallow Copy | Deep Copy |
|---|---|---|---|
| Скорость создания | Мгновенная O(1) | Линейная O(n) | Рекурсивная O(n*d), где d — глубина |
| Потребление памяти | Минимальное (только ссылка) | Пропорционально количеству элементов верхнего уровня | Пропорционально общему количеству объектов во всей структуре |
| Сценарии неэффективности | – | Множественное копирование одной структуры | Большая глубина вложенности, циклические ссылки |
| Масштабируемость | Отлично масштабируется | Хорошо масштабируется | Плохо масштабируется для глубоких структур |
Для реальных проектов важно применять оптимизации при работе с копированием объектов:
- Ленивое копирование — создание копии только при необходимости модификации (Copy-on-Write)
- Частичное глубокое копирование — реализация кастомных методов
__deepcopy__, копирующих только критичные компоненты - Использование неизменяемых структур — работа с immutable-объектами (tuple, frozenset) там, где это возможно
- Кэширование копий — повторное использование уже скопированных объектов для экономии ресурсов
При выборе метода копирования в высоконагруженных системах рекомендуется:
- Провести профилирование для выявления узких мест, связанных с копированием
- Использовать shallow copy для временных промежуточных операций, где нет риска изменения вложенных структур
- Применять deep copy для критичных данных, особенно в многопоточной среде
- Рассмотреть альтернативные паттерны проектирования (например, Immutable Objects, Flyweight), если копирование становится узким местом
Практические кейсы применения Shallow и Deep Copy
Выбор между поверхностным и глубоким копированием — это не только технический вопрос, но и архитектурное решение, влияющее на надёжность и производительность приложения. Рассмотрим конкретные сценарии, где критично правильно выбрать тип копирования. 🛠️
Когда использовать Shallow Copy:
- Кэширование неизменяемых данных — когда структура содержит только примитивы или immutable-объекты
- Временные копии для итерации — когда вам нужно обойти коллекцию без риска изменения исходных данных
- Обработка большого объёма данных — когда важна производительность, а вложенные структуры не модифицируются
- Паттерн "Снимок" (Memento) — для сохранения состояния объекта, когда вложенные объекты гарантированно неизменяемы
# Пример безопасного использования shallow copy в функциональном программировании
def process_data(original_data):
# Создаём копию для работы, не затрагивая оригинал
working_data = original_data.copy() # shallow copy
# Добавляем/удаляем элементы, но не модифицируем существующие вложенные объекты
working_data.append(calculated_value)
working_data.remove(outdated_item)
return working_data
Когда необходимо использовать Deep Copy:
- Многопоточная обработка данных — для изоляции данных между потоками
- Сохранение снимков состояния — для создания контрольных точек в состоянии приложения
- Защита от непреднамеренных изменений — при передаче данных между компонентами с разными зонами ответственности
- Обработка пользовательского ввода — для безопасной работы с данными от недоверенных источников
- Системы с отменой действий (undo/redo) — для хранения предыдущих состояний
# Пример использования deep copy в API
def get_default_config():
"""Возвращает настройки по умолчанию"""
default_config = {
"connection": {
"timeout": 30,
"retries": 3,
"backoff": [1, 2, 5, 10]
},
"processing": {
"threads": 4,
"batch_size": 100,
"options": {"debug": False}
}
}
# Используем deep copy, чтобы клиенты не могли влиять друг на друга,
# модифицируя полученную конфигурацию
return copy.deepcopy(default_config)
Практические рекомендации по выбору типа копирования в зависимости от ситуации:
- Аудит структуры данных: Проанализируйте глубину вложенности и типы объектов
- Оценка жизненного цикла: Определите, как долго будет существовать копия и кто имеет к ней доступ
- Анализ модификаций: Выясните, какие части данных будут изменяться после копирования
- Тестирование граничных случаев: Проверьте поведение при работе с циклическими ссылками или сложными объектами
- Измерение производительности: Оцените влияние выбранного метода на быстродействие системы
Для критических систем рекомендуется создать свой интерфейс копирования, инкапсулирующий логику выбора между shallow и deep copy:
class DataManager:
def __init__(self, data):
self._data = data
self._safe_mode = True
def get_data(self, safe=None):
"""
Возвращает копию данных в зависимости от режима безопасности.
Args:
safe: None использует глобальный режим, True для deep copy,
False для shallow copy
"""
use_safe = self._safe_mode if safe is None else safe
if use_safe:
return copy.deepcopy(self._data)
else:
return copy.copy(self._data)
def set_safe_mode(self, safe_mode):
"""Устанавливает режим безопасного копирования по умолчанию"""
self._safe_mode = bool(safe_mode)
В реальной разработке чаще всего встречаются гибридные подходы к копированию. Например, для оптимизации производительности в критичных участках кода можно реализовать функцию умного копирования, анализирующую структуру данных и выбирающую подходящий метод:
def smart_copy(obj, max_depth=1):
"""
Выполняет глубокое копирование до указанной максимальной глубины,
дальше использует поверхностное копирование
"""
if max_depth <= 0:
return copy.copy(obj)
if isinstance(obj, (list, tuple)):
return type(obj)(smart_copy(item, max_depth – 1) for item in obj)
if isinstance(obj, dict):
return {k: smart_copy(v, max_depth – 1) for k, v in obj.items()}
# Для остальных типов используем обычное копирование
return copy.copy(obj)
Правильный выбор между shallow и deep copy — это инвестиция в устойчивость вашего кода. При работе с неизменяемыми структурами и простыми типами данных поверхностное копирование экономит ресурсы без потери надёжности. Для сложных вложенных объектов, особенно если они модифицируются из разных частей программы, глубокое копирование остаётся единственным по-настоящему безопасным решением. Помните, что лучше потратить несколько лишних миллисекунд процессорного времени, чем дни на отладку трудноуловимых ошибок.
Таисия Ермакова
backend-разработчик