PythonMiddleTechnical

Объясните разницу между поверхностным (shallow copy) и глубоким (deep copy) копированием.

Shallow copy создаёт новый контейнер со ссылками на те же вложенные объекты; deepcopy рекурсивно клонирует всю иерархию. Используйте deepcopy, когда копия должна быть полностью независима от оригинала.

Shallow copy и deep copy в Python

Python предоставляет два механизма копирования объектов через модуль copy: copy.copy() для поверхностного и copy.deepcopy() для глубокого копирования.

Поверхностное копирование (shallow copy)

Создаётся новый объект-контейнер, но вложенные объекты не копируются — в новом контейнере хранятся ссылки на те же вложенные объекты, что и в оригинале.

import copy

original = [[1, 2, 3], [4, 5, 6]]
shallow = copy.copy(original)

shallow[0].append(99)  # меняем вложенный список
print(original[0])     # [1, 2, 3, 99] — оригинал тоже изменился!
print(shallow is original)   # False — разные контейнеры
print(shallow[0] is original[0])  # True — одни и те же вложенные объекты

Глубокое копирование (deep copy)

Рекурсивно создаёт новые объекты для всей иерархии. Изменение любой части копии не затрагивает оригинал.

import copy

original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)

deep[0].append(99)
print(original[0])  # [1, 2, 3] — оригинал не изменился
print(deep[0] is original[0])  # False — разные объекты

Встроенные способы создания shallow copy

Многие встроенные типы поддерживают поверхностное копирование без импорта copy:

lst = [1, [2, 3]]

# Все три эквивалентны shallow copy для списков:
a = lst[:]
b = list(lst)
c = lst.copy()

# Для словарей:
d = {"a": [1, 2]}
d_copy = d.copy()  # shallow

# Для множеств:
s = {1, 2, 3}
s_copy = s.copy()  # shallow (но элементы множества должны быть immutable)

Когда что применять

  • Shallow copy — когда вложенные объекты иммутабельны (числа, строки, кортежи без mutable элементов) или когда намеренно нужна общая ссылка на вложенную структуру.
  • Deep copy — когда нужна полная независимость копии: конфигурационные объекты, состояние игры, дерево разбора AST.

Кастомизация поведения

Класс может контролировать оба режима через специальные методы:

import copy

class MyConfig:
    def __init__(self, data: dict, secret: str):
        self.data = data
        self._secret = secret  # не хотим копировать

    def __copy__(self):
        # shallow: новый объект, data — та же ссылка
        new = MyConfig.__new__(MyConfig)
        new.data = self.data
        new._secret = self._secret
        return new

    def __deepcopy__(self, memo: dict):
        new = MyConfig.__new__(MyConfig)
        memo[id(self)] = new
        new.data = copy.deepcopy(self.data, memo)
        new._secret = self._secret  # намеренно не копируем
        return new

cfg = MyConfig({"key": [1, 2]}, "s3cr3t")
cfg_deep = copy.deepcopy(cfg)
print(cfg.data is cfg_deep.data)  # False
print(cfg._secret is cfg_deep._secret)  # True (строки — интернированы, ок)

Подводные камни

  • Циклические ссылки: deepcopy обрабатывает их через внутренний словарь memo, но самописные рекурсивные функции копирования зависнут. Всегда передавайте memo в рекурсивных __deepcopy__.
  • Иллюзия безопасности tuple: copy.copy((list1, list2)) возвращает тот же объект кортежа (CPython оптимизация), а вложенные списки не скопированы.
  • Производительность deepcopy: на больших вложенных структурах работает значительно медленнее — профилируйте перед применением в горячих путях.
  • Не все объекты копируемы: сокеты, файловые дескрипторы, потоки, функции с замыканиями на нескопируемые ресурсы поднимут TypeError.
  • dataclasses и copy: dataclasses.replace() делает shallow copy только указанных полей — это не то же самое, что copy.copy().
  • NumPy arrays: arr.copy() всегда делает deep copy данных массива, но объектные массивы (dtype=object) содержат ссылки — нужен deepcopy.
  • pickle vs deepcopy: pickle.loads(pickle.dumps(obj)) тоже даёт deep copy, но только для pickle-сериализуемых объектов; deepcopy работает шире.

Common mistakes

  • Описывать shallow vs deep copy только как термин и не показывать механизм на минимальном примере.
  • Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
  • Не связывать поведение с официальным контрактом Python и реальной эксплуатацией.

What the interviewer is testing

  • Объясняет shallow vs deep copy через последовательность действий, а не через набор ключевых слов.
  • Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics