Копирование: присваивание не копирует ничего, 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.
База: три операции на четырёх строках
Весь урок вырастает из четырёх строк кода. Стоит выписать их целиком и один раз посмотреть, что каждая делает.
import copy
a = [[1, 2], [3]] # список, внутри которого лежат другие списки
b = a # присваивание
c = copy.copy(a) # поверхностная копия
d = copy.deepcopy(a) # глубокая копияПока ничего не меняли, все четыре имени показывают на одинаковое содержимое, и
== между ними даёт True во всех сочетаниях. Разница появляется ровно в один
момент — когда меняют вложенный объект, а не верхний:
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 не копируют объекты, они создают связки между целью и объектом. Для коллекций, которые изменяемы или содержат изменяемые элементы, иногда нужна копия, чтобы можно было изменить одну копию, не изменяя другую.
Проверяется одной строкой:
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.
Поверхностная копия создаёт новый составной объект и затем, насколько это возможно, вставляет в него ссылки на объекты, найденные в оригинале.
A deep copy constructs a new compound object and then, recursively, inserts
copies into it of the objects found in the original.
Глубокая копия создаёт новый составной объект и затем рекурсивно вставляет в него копии объектов, найденных в оригинале.
Ссылки против копий. Всё остальное — следствия.
Переключите на картинке случай «общий объект»: у глубокой копии inner
окажется один на две ячейки. Потом «цикл»: у поверхностной единственная ячейка
покажет на оригинал, а не на себя.
Поверхностную копию делают четырьмя способами, и все четыре — одно и то же:
b = a[:]
b = list(a)
b = a.copy()
b = copy.copy(a)Разница между ними в пределах семи процентов на том же списке из тысячи записей — то есть её нет. А вот пятый способ, который часто пишут «заодно», обходится в 5,8 раза дороже остальных:
b = [x for x in a] # 13,7 мкс против 2,3–2,5 мкс у остальных четырёхПричина не в копировании: включение выполняет отдельный шаг цикла на каждый
элемент, а a[:] и list(a) копируют массив указателей целиком.
Классическая ловушка отсюда же — повторение списка:
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.
Поскольку глубокая копия копирует всё, она может скопировать слишком много — например, данные, которые предполагались общими для копий.
Решение названо там же — keeping a
(ведением словаря memo объектов, уже скопированных в текущем проходе копирования).memo dictionary of objects already copied during the current copying pass
Что из этого следует на практике:
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 — но уже другойСтруктура воспроизведена, а не размножена. Две отдельные глубокие копии при этом между собой ничего не делят: таблица живёт один проход и на следующем вызове заводится заново.
Ту же таблицу можно заполнить заранее — и тем самым запретить копировать конкретный объект:
memo = {id(shared): shared}
kept = copy.deepcopy(orig, memo)
print(kept["a"] is shared) # TrueЭто способ сказать «скопируй всё, кроме вот этого», не трогая сами классы:
второй аргумент deepcopy для того и открыт. Второй способ — перехват
__deepcopy__, и он разобран ниже.
И тот же memo — причина, по которой цикл переживает копирование:
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 вернул тот же | 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__ не вызывается
Это следствие, о котором узнают поздно и дорого.
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__ напрямую. Значит:
- проверки аргументов, стоящие в конструкторе, не выполняются;
- ресурсы, которые конструктор открывает, не открываются — они копируются как обычные поля. У поверхностной копии сокет тот же самый. Глубокая исключения для него не делает: она пытается скопировать сокет как обычное поле, и на настоящем сокете это обычно кончается ошибкой. Ровно поэтому документация и перечисляет такие типы отдельно.
Если у объекта есть что-то, чего копировать нельзя, перехват пишется руками:
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__ нужно сделать глубокую копию составной части, она должна вызвать функцию deepcopy(), передав эту часть первым аргументом, а словарь memo — вторым). Без второго аргумента разваливается ровно то, ради чего таблица и заведена.__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
Копирование — не восстановление инварианта
Отсюда следует практическое правило, которое стоит произносить целиком: копия — это снимок полей, а не заново собранный объект. Всё, за что отвечал конструктор — проверка dsn, открытый сокет, зарегистрированный обработчик, — в копии либо отсутствует, либо разделено с оригиналом.
Поэтому у deepcopy есть соседи, которые часто оказываются нужнее. Это не
замены — они решают другие задачи, и выбирают между ними по задаче, а не по
цене:
| что нужно | чем делать |
|---|---|
| объект того же типа с парой изменённых полей | dataclasses.replace(obj, field=value) — вызывает __init__ |
| объект, собранный заново из данных | своя фабрика или конструктор |
| чтобы копия не понадобилась вовсе | неизменяемые значения: копировать нечего |
| копия, пережившая процесс или сеть | сериализация; это уже не копирование, а другой формат |
deepcopy остаётся там, где нужен именно снимок графа со всеми его
внутренними связями — и там ему замены нет.
Глубже: что это стоит
Из картинки следует не «берите 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)Практика · оцените
Проверьте себя
orig = {'a': shared, 'b': shared}, где shared — один словарь. Сколько объектов-словарей внутри copy.deepcopy(orig)?
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Три операции над одной структурой дают три разных результата, и на плоском списке разница не видна вовсе — поэтому их и путают.
b = aне копирует ничего: появляется второе имя для того же объекта, и справочник модуля говорит это прямо — Assignment statements in Python do not copy objects, they create bindings between a target and an object.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, и растёт она по числу объектов внутри, а не по объёму данных.
На самом деле
- Для списка это одно и то же:
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 aTrue,b[0] is bFalse. Единственная ячейка копии показывает на ОРИГИНАЛ; сама копия циклической не является. Это прямое следствие определения «вставляет ссылки на объекты, найденные в оригинале» — просто применённое к случаю, где найденный объект и есть оригинал. - Ноль вызовов на две копии, проверено счётчиком в конструкторе. И
copy, иdeepcopyидут через__reduce_ex__и восстанавливают__dict__напрямую. Поэтому копия объекта с открытым сокетом получает тот же сокет (поверхностная) или попытку скопировать сокет (глубокая), а не новое соединение. - У
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. Поведение не изменилось ни в одной строке замера; знать об этом стоит только затем, чтобы не удивиться, открыв исходник на разных версиях и увидев разный код.<
Что разобрано
- База: три операции на четырёх строках
- Механизм 1: присваивание не копирует
- Механизм 2: один уровень — и что это значит
- Механизм 3: глубокая копия строит копию графа, а не обходит ссылки поодиночке
- Механизм 4: чего не копирует ни та ни другая
- Механизм 5: `__init__` не вызывается
- Глубже: что это стоит
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Модуль 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
- Модуль 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
- 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
- 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
- Модуль 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
- Модуль 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