deepcopy сохранил общую ссылку: как копировать граф настроек
У двух заданий одинаковые настройки повторных попыток. Разработчик собирает общий шаблон, делает deepcopy и меняет число попыток первого задания. Второе меняется вместе с ним. Исходный шаблон при этом остаётся целым, и кажется, будто глубокое копирование сработало лишь наполовину.
Посмотреть на связи между объектами
Словарь хранит ссылки на значения. Если один и тот же вложенный словарь записан под двумя ключами, в памяти существует один объект с двумя путями доступа. Это отличается от двух равных словарей. Равенство отвечает, совпадает ли содержимое сейчас; оператор is позволяет проверить, действительно ли оба пути приводят к одному объекту.
Глубокое копирование обходит структуру и запоминает уже обработанные объекты. Когда встречается повторная ссылка, используется уже созданная копия. Благодаря этому поддерживаются циклические структуры и сохраняются внутренние связи графа. Новая структура отделяется от исходной, однако сама связь двух её ветвей не исчезает. Именно такую картину и стоит ожидать от общего шаблона.
Перед выбором исправления решите, зачем связь появилась. Иногда она намеренная: несколько обработчиков должны видеть единый объект политики. Тогда глубокая копия общего графа ведёт себя правильно. Иногда общий словарь оказался сокращением записи при подготовке независимых заданий. В этом случае ошибка находится в способе построения модели, и копирование всего графа её аккуратно переносит.
Создать независимые ветви явно
В примере сначала воспроизводится общий узел. У copied оба задания продолжают делить retry, но исходный template не меняется. Затем каждая ветвь копируется отдельным вызовом deepcopy. Между этими вызовами нет общего обхода, поэтому создаются независимые вложенные словари. Последнее изменение влияет только на первое задание. Проверяем и значения, и идентичность объектов.
Для простого объекта ещё яснее фабрика, которая каждый раз создаёт новую структуру настроек. Она показывает намерение при чтении кода. deepcopy полезен, когда вход уже содержит произвольную вложенную структуру и необходимо отделить её от источника. Не превращайте его в автоматический ответ на любой вопрос владения: сначала определите, какие связи должны остаться общими.
from copy import deepcopy
retry = {"attempts": 3, "delays": [1, 2]}
template = {"first": {"retry": retry}, "second": {"retry": retry}}
copied = deepcopy(template)
assert copied["first"]["retry"] is copied["second"]["retry"]
assert copied["first"]["retry"] is not retry
copied["first"]["retry"]["attempts"] = 7
assert copied["second"]["retry"]["attempts"] == 7
assert retry["attempts"] == 3
independent = {key: deepcopy(value) for key, value in template.items()}
independent["first"]["retry"]["delays"].append(5)
assert independent["first"]["retry"] is not independent["second"]["retry"]
assert independent["second"]["retry"]["delays"] == [1, 2]
assert retry["delays"] == [1, 2]
print(independent)Копировать данные на понятной границе
Практическая граница часто проходит между принятым конфигурационным документом и рабочими заданиями. Сначала загрузчик проверяет структуру, затем фабрика создаёт состояние каждого задания. После этой точки обработчики не должны случайно менять общий исходник. Такое правило проще сопровождать, чем вызовы deepcopy в разных методах, появляющиеся по мере обнаружения взаимного влияния.
Нельзя бездумно переносить этот приём на объекты соединений, блокировок и файлов. У таких значений есть внешний ресурс или особые правила жизни. Модель задания лучше разделить на обычные данные и явно переданные зависимости. Копировать описание запроса разумно; пытаться получить отдельное соединение копированием объекта клиента — неверная модель получения ресурса.
Регрессионный тест должен изменять вложенный список или словарь после создания двух заданий. Проверка только верхнего уровня не поймает связь глубже. Отдельно убедитесь, что источник остался прежним. Когда в модели общие узлы необходимы, добавьте обратное утверждение через is: оно защитит важную связь от будущего рефакторинга, который начнёт разрывать граф.
На практике расследование удобно начинать с маленького снимка модели. Выберите два пути к подозрительному значению и сравните их через is до изменения. После этого выполните ту же проверку для скопированной структуры и отдельно сравните копию с источником. Три наблюдения разделяют разные дефекты: общие ветви внутри нового объекта, связь нового объекта со старым и просто одинаковое содержимое независимых объектов. Для большого графа не обязательно печатать все данные, среди которых могут быть секреты. Достаточно зафиксировать идентичность нужных узлов и безопасные признаки структуры. Если настройки должны быть общими только на чтение, рассмотрите неизменяемую модель и создание новой версии при изменении: тогда сама операция изменения перестанет незаметно затрагивать соседние задания.
Следующий шаг в практике
В бесплатном уроке о классе и атрибутах экземпляра создайте фабрику состояния задания и проверьте два независимых экземпляра. Откройте программу продвинутого Python, чтобы продолжить моделированием объектов и регрессионными тестами. Для дальнейшего разбора памяти и механизмов интерпретатора есть «Python для профессионалов».
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
