Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·30 МИН

Копирование: присваивание не копирует ничего, copy — один уровень, deepcopy — граф целиком

Три операции, которые путают между собой, потому что на плоском списке они дают одинаковый результат. Разница видна только на вложенности — и на ней же выясняется, что глубокая копия не «копирует всё подряд»: общий объект остаётся общим, цикл остаётся циклом, а __init__ не вызывается ни разу.

Полное техническое изложение

TL;DR

Три операции над одной структурой дают три разных результата, и на плоском списке разница не видна вовсе — поэтому их и путают. b = a не копирует ничего: появляется второе имя для того же объекта, и справочник модуля говорит это прямо — Assignment statements in Python do not copy objects, they create bindings between a target and an object (Операторы присваивания в Python не копируют объекты, они создают связки между целью и объектом). copy.copy(a) создаёт новый верхний объект, но кладёт в него те же вложенные. copy.deepcopy(a) добирается и до вложенных.

Отсюда главное следствие: разница видна только тогда, когда меняют вложенное. Запись в b[0] после поверхностной копии видна в a. И два случая, которых обычно не ждут: __init__ не вызывается ни разу — ни при поверхностной копии, ни при глубокой, так что инварианты, которые ставит конструктор, не восстанавливаются; и поверхностная копия списка, содержащего сам себя, содержит оригинал, а не себя: b[0] is a даёт True, b[0] is b — False, то есть циклической структуры больше нет.

Дальше — то, что отличает знающего от читавшего. Глубокая копия не «копирует всё подряд»: она строит копию графа по протоколу копирования, запоминая уже скопированные объекты в таблице memo. Отсюда сразу два свойства: цикл не уводит копирование в бесконечность, а объект, лежавший под двумя ключами, остаётся одним и в копии. Цена: на списке из тысячи трёхуровневых записей поверхностная копия — 2,35 мкс, глубокая — 4252 мкс (3.13.7). Разница ×1809, и растёт она по числу объектов внутри, а не по объёму данных.

Порог входа
Перед уроком достаточно понимать
  • что список или словарь можно изменить на месте, не создавая нового объекта;
  • что значение может лежать внутри другого значения — список внутри списка, словарь внутри словаря;
  • что две переменные могут указывать на один и тот же объект, и тогда изменение видно через обе.
Заранее знать не нужно
  • чем поверхностная копия отличается от глубокой, что такое протокол копирования и граф объектов;
  • memo, __reduce_ex__, __deepcopy__, атомарные типы, copy.replace.

База: три операции на четырёх строках

Весь урок вырастает из четырёх строк кода. Стоит выписать их целиком и один раз посмотреть, что каждая делает.

PYTHON
import copy
 
a = [[1, 2], [3]]         # список, внутри которого лежат другие списки
b = a                     # присваивание
c = copy.copy(a)          # поверхностная копия
d = copy.deepcopy(a)      # глубокая копия

Пока ничего не меняли, все четыре имени показывают на одинаковое содержимое, и == между ними даёт True во всех сочетаниях. Разница появляется ровно в один момент — когда меняют вложенный объект, а не верхний:

PYTHON
a[0].append(99)           # меняем список, лежащий ВНУТРИ
 
b[0]                      # [1, 2, 99] — b и есть a, другого объекта не появилось
c[0]                      # [1, 2, 99] — верхний список новый, а вложенный тот же
d[0]                      # [1, 2]     — у глубокой копии свой вложенный список

Три разных ответа на одно изменение. Читать их стоит так:

  • b = a не создаёт объекта вовсе: это второе имя для того же списка;
  • cновый верхний список, но внутри у него те же самые вложенные списки, что и у a;
  • d — новый верхний список, у которого и вложенные тоже новые.

И вот главный вопрос урока: на плоском списке эти три операции неотличимы — так до какой глубины на самом деле доходит каждая? Ответ уровня «база»: присваивание — ни до какой, поверхностная копия — до первого уровня, глубокая — дальше него.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, где проходит граница «дальше него»: что глубокая копия делает с объектом, который встретился ей дважды, что бывает со структурой, ссылающейся сама на себя, чего не копирует ни та ни другая операция и почему конструктор при копировании не вызывается ни разу.

Механизм 1: присваивание не копирует

контракт языкаГарантия языка: присваивание связывает имя с тем же объектом. Копия начинается там, где её попросили явно.

Это первое место, где путаница начинается, и оно же самое простое. b = a не создаёт никакого нового объекта — появляется второе имя для одного и того же.

Assignment statements in Python do not copy objects, they create bindings between a target and an object. For collections that are mutable or contain mutable items, a copy is sometimes needed so one can change one copy without changing the other.

перевод

Операторы присваивания в Python не копируют объекты, они создают связки между целью и объектом. Для коллекций, которые изменяемы или содержат изменяемые элементы, иногда нужна копия, чтобы можно было изменить одну копию, не изменяя другую.

Модуль copy, вводный абзац

Проверяется одной строкой:

PYTHON
a = [1, 2, 3]
b = a
b.append(4)
print(a)        # [1, 2, 3, 4]
print(b is a)   # True

Отсюда правило, к которому всё сведётся: вопрос «копия или не копия» — это всегда вопрос про is, а не про ==. Равенство здесь ничего не различает: у оригинала, поверхностной и глубокой копии == даёт True во всех трёх случаях.

Механизм 2: один уровень — и что это значит

Определения из документации отличаются ровно одним словом, и это слово стоит прочитать буквально.

A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

перевод

Поверхностная копия создаёт новый составной объект и затем, насколько это возможно, вставляет в него ссылки на объекты, найденные в оригинале.

Модуль copy, раздел о разнице

A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

перевод

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

Там же

Ссылки против копий. Всё остальное — следствия.

Переключите на картинке случай «общий объект»: у глубокой копии inner окажется один на две ячейки. Потом «цикл»: у поверхностной единственная ячейка покажет на оригинал, а не на себя.

Поверхностную копию делают четырьмя способами, и все четыре — одно и то же:

PYTHON
b = a[:]
b = list(a)
b = a.copy()
b = copy.copy(a)

Разница между ними в пределах семи процентов на том же списке из тысячи записей — то есть её нет. А вот пятый способ, который часто пишут «заодно», обходится в 5,8 раза дороже остальных:

PYTHON
b = [x for x in a]      # 13,7 мкс против 2,3–2,5 мкс у остальных четырёх

Причина не в копировании: включение выполняет отдельный шаг цикла на каждый элемент, а a[:] и list(a) копируют массив указателей целиком.

Классическая ловушка отсюда же — повторение списка:

PYTHON
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)     # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

Внешнее * 3 не создало три строки. Оно создало три ссылки на одну строку — ровно то же, что делает поверхностная копия, только записанное короче.

Механизм 3: глубокая копия строит копию графа, а не обходит ссылки поодиночке

Вот место, где почти все объяснения врут. «Глубокая копия копирует всё дерево» и «копирует всё подряд» — неверно, и неверно дважды: во-первых, у структуры может не быть формы дерева вовсе, во-вторых, копируется не каждая встреченная ссылка. Точнее так: глубокая копия строит копию графа объектов, спрашивая у каждого встреченного объекта, как его копировать, и запоминая уже скопированное. Первое — протокол копирования: для каждого типа находится способ, а объект может и перехватить его сам. Второе — таблица, с которой deepcopy сверяется на каждом объекте.

Из этой формулировки сразу следуют два свойства, которые из «копирует всё дерево» не следуют никак: общий объект остаётся общим и в копии, а цикл не уводит копирование в бесконечность.

Because deep copy copies everything it may copy too much, such as data which is intended to be shared between copies.

перевод

Поскольку глубокая копия копирует всё, она может скопировать слишком много — например, данные, которые предполагались общими для копий.

Модуль copy, о двух проблемах глубокого копирования

Решение названо там же — keeping a memo dictionary of objects already copied during the current copying pass (ведением словаря memo объектов, уже скопированных в текущем проходе копирования).

Что из этого следует на практике:

PYTHON
shared = {"config": 1}
orig = {"a": shared, "b": shared}
deep = copy.deepcopy(orig)
 
print(orig["a"] is orig["b"])       # True — один объект на два ключа
print(deep["a"] is deep["b"])       # True — и в копии он тоже ОДИН
print(deep["a"] is orig["a"])       # False — но уже другой

Структура воспроизведена, а не размножена. Две отдельные глубокие копии при этом между собой ничего не делят: таблица живёт один проход и на следующем вызове заводится заново.

Ту же таблицу можно заполнить заранее — и тем самым запретить копировать конкретный объект:

PYTHON
memo = {id(shared): shared}
kept = copy.deepcopy(orig, memo)
print(kept["a"] is shared)          # True

Это способ сказать «скопируй всё, кроме вот этого», не трогая сами классы: второй аргумент deepcopy для того и открыт. Второй способ — перехват __deepcopy__, и он разобран ниже.

И тот же memo — причина, по которой цикл переживает копирование:

PYTHON
a = []
a.append(a)
 
s = copy.copy(a)
d = copy.deepcopy(a)
 
print(s[0] is a, s[0] is s)     # True  False
print(d[0] is a, d[0] is d)     # False True

Поверхностная копия списка, который содержал сам себя, сама себя не содержит: её единственная ячейка показывает на оригинал. Циклической структуры больше нет, хотя копировали именно её. Глубокая — цикл сохранила.

Механизм 4: чего не копирует ни та ни другая

Список назван в документации явно.

This module does not copy types like module, method, stack trace, stack frame, file, socket, window, or any similar types. It does "copy" functions and classes (shallow and deeply), by returning the original object unchanged; this is compatible with the way these are treated by the pickle module.

перевод

Этот модуль не копирует такие типы, как модуль, метод, трассировка стека, кадр стека, файл, сокет, окно и любые подобные. Он «копирует» функции и классы (и поверхностно, и глубоко), возвращая исходный объект без изменений; это согласуется с тем, как с ними обходится модуль pickle.

Модуль copy, о границах

И здесь же — расхождение, которое каждый раз выглядит как ошибка интерпретатора:

объектcopy.copy вернул тот жеdeepcopy вернул тот же
int, str, range, функция, типдада
кортеж из неизменяемогодада
кортеж со списком внутриданет
frozensetданет
sliceданет

Неизменяемость тут ни при чём. В Lib/copy.py два разных набора атомарных типов: у copy в нём есть tuple, frozenset, slice и super, у deepcopy — нет. frozenset и slice из-за этого идут длинным путём через __reduce_ex__, и deepcopy строит новый объект. Кортеж — отдельный случай, о нём сразу ниже. Результат одинаков на 3.11, 3.12, 3.13 и 3.14.7.

Кортеж из неизменяемого — отдельный случай, и он не про наборы типов: _deepcopy_tuple копирует элементы, сравнивает каждый с исходным и, если не изменился ни один, возвращает исходный кортеж.

Механизм 5: __init__ не вызывается

Это следствие, о котором узнают поздно и дорого.

PYTHON
class Connection:
    def __init__(self, dsn):
        self.dsn = dsn
        self.socket = open_socket(dsn)
 
c1 = Connection("postgres://…")
c2 = copy.copy(c1)
c3 = copy.deepcopy(c1)

Ни одна из копий не вызывает __init__ — ноль вызовов на две копии, проверено счётчиком. Обе идут через __reduce_ex__ и восстанавливают __dict__ напрямую. Значит:

  • проверки аргументов, стоящие в конструкторе, не выполняются;
  • ресурсы, которые конструктор открывает, не открываются — они копируются как обычные поля. У поверхностной копии сокет тот же самый. Глубокая исключения для него не делает: она пытается скопировать сокет как обычное поле, и на настоящем сокете это обычно кончается ошибкой. Ровно поэтому документация и перечисляет такие типы отдельно.

Если у объекта есть что-то, чего копировать нельзя, перехват пишется руками:

PYTHON
class Connection:
    def __deepcopy__(self, memo):
        new = Connection.__new__(Connection)
        memo[id(self)] = new                       # ДО обхода полей
        new.dsn = copy.deepcopy(self.dsn, memo)    # memo передаётся дальше
        new.socket = open_socket(new.dsn)
        return new

Две строчки здесь неочевидны, и обе документированы. Запись в memo до обхода полей — иначе объект, ссылающийся на себя, уйдёт в бесконечную рекурсию. И передача memo вторым аргументом: If the __deepcopy__ implementation needs to make a deep copy of a component, it should call the deepcopy() function with the component as first argument and the memo dictionary as second argument (Если реализации __deepcopy__ нужно сделать глубокую копию составной части, она должна вызвать функцию deepcopy(), передав эту часть первым аргументом, а словарь memo — вторым). Без второго аргумента разваливается ровно то, ради чего таблица и заведена.

Копирование — не восстановление инварианта

Отсюда следует практическое правило, которое стоит произносить целиком: копия — это снимок полей, а не заново собранный объект. Всё, за что отвечал конструктор — проверка dsn, открытый сокет, зарегистрированный обработчик, — в копии либо отсутствует, либо разделено с оригиналом.

Поэтому у deepcopy есть соседи, которые часто оказываются нужнее. Это не замены — они решают другие задачи, и выбирают между ними по задаче, а не по цене:

что нужночем делать
объект того же типа с парой изменённых полейdataclasses.replace(obj, field=value) — вызывает __init__
объект, собранный заново из данныхсвоя фабрика или конструктор
чтобы копия не понадобилась вовсенеизменяемые значения: копировать нечего
копия, пережившая процесс или сетьсериализация; это уже не копирование, а другой формат

deepcopy остаётся там, где нужен именно снимок графа со всеми его внутренними связями — и там ему замены нет.

Глубже: что это стоит

наблюдение замераbench/copy/cost.py, CPython 3.13.7. Абсолютные времена зависят от машины и от формы графа; содержательна кратность.

Из картинки следует не «берите pickle», а два правила.

Запись в замере — это {"id": i, "tags": ["a", "b"], "meta": {"x": i}}: три уровня и четыре объекта на запись, то есть четыре тысячи объектов на тысячу записей. Отсюда и ×1809 — то есть это множитель для такой формы данных, а не свойство глубокой копии вообще: на другой форме он будет другим. Переносить стоит не его, а зависимость, из которой он получился.

Первое: цена глубокой копии растёт по числу объектов, а не по объёму данных. Десять тысяч чисел в списке копируются за 2,3 мс, десять тысяч списков по одному числу — за 10 мс. Чисел столько же, объём почти тот же; во втором случае прибавились десять тысяч списков, которые надо обойти и создать заново. Сами int не копируются вовсе — они атомарны и возвращаются как есть.

Второе: глубокая копия рекурсивна. На вложенности 498 при стандартном sys.getrecursionlimit() == 1000 она падает с RecursionError — то есть тратит два кадра стека на уровень. Дерево из пятисот уровней скопировать не получится, и выясняется это уже на боевом сервере.

Отдельно про быстрые замены. pickle туда-обратно быстрее глубокой копии в 5,8 раза — это другое сравнение, чем «включение против среза» выше, отношение просто совпало. Он сохраняет и общность объектов, и циклы, но требует, чтобы объект пиклился, — а сокет, файл и замыкание не пиклятся. json быстрее в 2,7 раза и молча меняет типы: {1: (2, 3)} возвращается как {'1': [2, 3]}, а на цикле падает с ValueError. Ручное копирование по известной форме быстрее в 14,2 раза ровно потому, что форма вписана в него руками — и живёт до первого нового поля.

Как отвечать на собеседовании

Короткий ответ: присваивание не копирует ничего — это второе имя для того же объекта; copy.copy создаёт новый верхний объект и оставляет вложенные общими; copy.deepcopy добирается и до вложенных. Разница видна только тогда, когда меняют вложенный объект: после поверхностной копии изменение видно через обе стороны, после глубокой — нет.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Точная формулировка про глубокую копию звучит не как «копирует всё дерево», а как «строит копию графа объектов по протоколу копирования, запоминая уже скопированные». Отсюда сразу два свойства, ради которых её и уточняют: цикл не уводит копирование в бесконечность, а объект, лежавший под двумя ключами, остаётся одним и в копии — deep["a"] is deep["b"] даёт True.

И три вещи, которые редко называют, а стоят они дороже всего: __init__ не вызывается ни разу; deepcopy рекурсивна и ограничена пределом стека; цена считается по числу объектов внутри, а не по объёму данных.

Дальше спросят

Спросят дальше

В структуре есть цикл: объект ссылается сам на себя. deepcopy зациклится?

Короткий ответ

Нет. deepcopy копирует граф, а не каждую ссылку по отдельности: уже скопированные объекты он помнит, поэтому цикл замыкается на копию, а два разных пути к одному объекту дают в копии один объект, а не два.

Спросят дальше

При копировании вызовется __init__?

Короткий ответ

Нет, и это регулярный источник сюрпризов: объект собирается в обход конструктора, поэтому всё, что __init__ делал помимо присваивания полей, при копировании не происходит.

Спросят дальше

Есть что-то, чего не скопирует ни поверхностная, ни глубокая копия?

Короткий ответ

Да, и в уроке это отдельным разделом: часть содержимого объекта копированием не переносится вовсе, независимо от выбранного способа.

Частые заблуждения

Утверждение

Срез a[:] и copy.copy(a) взаимозаменяемы

На самом деле

Для списка это одно и то же: Lib/copy.py отправляет список прямо в list.copy. Замер подтверждает: a[:], list(a), a.copy() и copy.copy(a) различаются в пределах 7 % на списке из тысячи элементов. Разница появляется на СВОИХ классах: copy.copy ищет __copy__ и __reduce_ex__, а среза у произвольного объекта нет вовсе.

Утверждение

Глубокая копия копирует каждую ссылку отдельно

На самом деле

Проверено: если один словарь лежал под двумя ключами, в глубокой копии он тоже ОДИН — deep["a"] is deep["b"] даёт True. Копируется граф, а не обход, и держит это таблица memo, о которой документация говорит прямо. Обратная сторона — цикл: список, содержащий сам себя, после deepcopy содержит СЕБЯ, а не оригинал.

Утверждение

Поверхностная копия циклической структуры даёт такую же циклическую структуру

На самом деле

Даёт не такую же. a = []; a.append(a), затем b = copy.copy(a) — и b[0] is a True, b[0] is b False. Единственная ячейка копии показывает на ОРИГИНАЛ; сама копия циклической не является. Это прямое следствие определения «вставляет ссылки на объекты, найденные в оригинале» — просто применённое к случаю, где найденный объект и есть оригинал.

Утверждение

Копия объекта проходит через __init__

На самом деле

Ноль вызовов на две копии, проверено счётчиком в конструкторе. И copy, и deepcopy идут через __reduce_ex__ и восстанавливают __dict__ напрямую. Поэтому копия объекта с открытым сокетом получает тот же сокет (поверхностная) или попытку скопировать сокет (глубокая), а не новое соединение.

Утверждение

deepcopy неизменяемого объекта всегда возвращает его же

На самом деле

У frozenset и slice — нет: copy.copy возвращает тот же объект, copy.deepcopy строит новый. Причина не в неизменяемости, а в том, что в Lib/copy.py ДВА разных набора атомарных типов, и эти типы есть только в наборе copy. Одинаково на 3.11–3.14. У кортежа поведение противоположное и по другой причине: _deepcopy_tuple сравнивает скопированные элементы с исходными и, если не изменился ни один, возвращает исходный кортеж.

История версий

ВерсияИзменениеЧто это значит для кода
3.11Опорная точка урока. Всё поведение, разобранное выше, — глубина копирования, сохранение общности и циклов, расхождение по frozenset и slice, отсутствие вызова __init__ — на 3.11 уже такое же, как на 3.14. Проверено запуском одного и того же скрипта на четырёх версиях.
3.13Добавлена copy.replace(obj, **changes) — третья функция модуля, о которой обычно не знают. Она не копирует дерево: создаёт новый объект того же типа с заменёнными полями и работает только с namedtuple, датаклассами и классами, определившими __replace__. Там, где нужно «то же самое, но с другим полем», она заменяет связку deepcopy + присваивание.
3.14Развилка в Lib/copy.py переписана: вместо словаря _copy_dispatch — проверки принадлежности множествам _copy_atomic_types и _copy_builtin_containers. Поведение не изменилось ни в одной строке замера; знать об этом стоит только затем, чтобы не удивиться, открыв исходник на разных версиях и увидев разный код.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Один и тот же словарь лежит под двумя ключами. Что напечатает этот код?
import copy

inner = {"n": 1}
outer = {"a": inner, "b": inner}
deep = copy.deepcopy(outer)
shallow = copy.copy(outer)

print(deep["a"] is deep["b"])
print(deep["a"] is inner)
print(shallow["a"] is inner)

Практика · оцените

Дерево из пятидесяти словарей со списками внутри: copy.copy против copy.deepcopy. Во сколько раз дороже глубокая копия?
раз

Проверьте себя

Вопрос 1 из 5

orig = {'a': shared, 'b': shared}, где shared — один словарь. Сколько объектов-словарей внутри copy.deepcopy(orig)?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Модуль copy — поверхностное и глубокое копированиеОфициальная документация. Первоисточник всех определений урока. Про присваивание дословно: «Assignment statements in Python do not copy objects, they create bindings between a target and an object» (Операторы присваивания в Python не копируют объекты, они создают связки между целью и объектом). Про поверхностную копию: «A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original» (Поверхностная копия создаёт новый составной объект и затем, насколько это возможно, вставляет в него ССЫЛКИ на объекты, найденные в оригинале). Про глубокую: «A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original» (Глубокая копия создаёт новый составной объект и затем рекурсивно вставляет в него КОПИИ объектов, найденных в оригинале).https://docs.python.org/3.14/library/copy.html
  2. Модуль copy — таблица memo и то, чего deepcopy не копируетОфициальная документация. Откуда взяты разделы про общность и про цикл. Документация прямо называет обе проблемы своими именами: «Because deep copy copies everything it may copy too much, such as data which is intended to be shared between copies» (Поскольку глубокая копия копирует всё, она может скопировать слишком много — например, данные, которые предполагались общими для копий), и тут же говорит, чем это решено: «keeping a memo dictionary of objects already copied during the current copying pass» (ведением словаря memo объектов, уже скопированных в текущем проходе копирования). Там же перечислено, что модуль не копирует вовсе: «This module does not copy types like module, method, stack trace, stack frame, file, socket, window, or any similar types» (Этот модуль не копирует такие типы, как модуль, метод, трассировка стека, кадр стека, файл, сокет, окно и любые подобные).https://docs.python.org/3.14/library/copy.html
  3. Lib/copy.py — два разных списка атомарных типовИсходный код CPython. Место, объясняющее расхождение, которое иначе выглядит как ошибка. У copy свой набор — _copy_atomic_types, в нём есть tuple, frozenset, slice и super; у deepcopy свой — _atomic_types, и этих четырёх в нём нет. Поэтому copy.copy(frozenset(...)) возвращает тот же объект, а copy.deepcopy(frozenset(...)) — новый, хотя frozenset неизменяем в обоих случаях. На 3.13 та же развилка записана иначе (словарь _copy_dispatch и цикл по типам), а результат тот же — проверено запуском на 3.11, 3.12, 3.13 и 3.14.7. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Lib/copy.py
  4. Lib/copy.py — _deepcopy_tuple и порядок поиска в deepcopyИсходный код CPython. Функция _deepcopy_tuple объясняет, почему deepcopy кортежа из неизменяемого содержимого возвращает ТОТ ЖЕ кортеж: если ни один элемент не изменился после копирования, возвращается оригинал. Оттуда же порядок поиска обработчика в deepcopy: атомарный тип, таблица типов, подкласс type, __deepcopy__, copyreg.dispatch_table, __reduce_ex__(4), __reduce__. Тег CPython 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Lib/copy.py
  5. Модуль copy — протокол __copy__ и __deepcopy__Официальная документация. Спецификация обоих перехватов и требование к тому, как писать __deepcopy__: «If the __deepcopy__ implementation needs to make a deep copy of a component, it should call the deepcopy() function with the component as first argument and the memo dictionary as second argument» (Если реализации __deepcopy__ нужно сделать глубокую копию составной части, она должна вызвать функцию deepcopy(), передав эту часть первым аргументом, а словарь memo — вторым). Без второго аргумента разваливается именно то, ради чего memo и заведён.https://docs.python.org/3.14/library/copy.html
  6. Модуль copy — copy.replace, добавлен в 3.13Официальная документация. Третья функция модуля, о которой обычно не знают: «Creates a new object of the same type as obj, replacing fields with values from changes» (Создаёт новый объект того же типа, что obj, заменяя поля значениями из changes), с пометкой «Added in version 3.13» (Добавлено в версии 3.13). Работает только с namedtuple, dataclasses и классами, определившими __replace__.https://docs.python.org/3.14/library/copy.html