Сравнение словарей и копирование
Словари сравниваются по парам — порядок не важен, а копировать их нужно аккуратно: copy() делит вложенные значения, deepcopy копирует насквозь.
Редакция Питоники
К этому моменту словари создавались, менялись, объединялись и сортировались. Остались два вопроса, на которые каждый рано или поздно напарывается: как сравнить два словаря и как получить их копию. Оба вопроса обманчиво просты — у == и у копий есть нюанс, из-за которого программы ведут себя не так, как ждёт автор. Нюанс этот — разница между «одинаковые данные» и «один объект».
== по парам: порядок не важен
menu = {'чай': 50, 'кофе': 120}
same_menu = {'кофе': 120, 'чай': 50}
print(menu == same_menu)
twin = dict(menu)
print(twin == menu)
print(twin is menu)
True True False
Два словаря равны, когда у них одинаковые пары, а порядок пар при сравнении не важен. Меню записали в разном порядке — сравнение вернуло True. Оператор == отвечает за содержимое, оператор is — за другой вопрос: не два ли имени у одного объекта. twin, собранный через dict(menu), содержит те же пары, но это новый объект — поэтому == даёт True, а is даёт False. is пригодится, когда важно различать копию и оригинал, а == — для сравнения данных: сохранённые настройки против текущих, ожидаемый ответ API против полученного.
copy(): независимая копия верхнего уровня
original = {'lang': 'ru', 'theme': 'dark'}
copied = original.copy()
copied['lang'] = 'en'
print(original)
print(copied)
{'lang': 'ru', 'theme': 'dark'}
{'lang': 'en', 'theme': 'dark'}Ловушка: вложенные значения общие
original = {'settings': {'theme': 'dark'}}
copied = original.copy()
copied['settings']['theme'] = 'light'
print(original)
print(copied)
{'settings': {'theme': 'light'}}
{'settings': {'theme': 'light'}}Правка по верхнему уровню осталась в копии, а правка вложенного словаря дошла и до оригинала — объяснение в самой записи. Метод copy() поверхностный: он создал новый словарь и переложил в него те же ключи, а под ключом 'settings' хранится ссылка — тот же вложенный словарь, один на обе переменные. Новая обёртка, общая начинка. Это не баг и не лень авторов: для плоских словарей copy() достаточно, а глубокое копирование стоит дорого, поэтому Python оставляет выбор программисту.
copy.deepcopy: копия насквозь
import copy
original = {'settings': {'theme': 'dark'}}
copied = copy.deepcopy(original)
copied['settings']['theme'] = 'light'
print(original)
print(copied)
{'settings': {'theme': 'dark'}}
{'settings': {'theme': 'light'}}Что именно сравнивает ==
Сравнение словарей устроено глубже, чем кажется. == отвечает на вопрос «есть ли в обоих словарях одни и те же пары», и проверяет это рекурсивно: значения сравниваются тоже, а если значения — словари, то и они сравниваются по тем же правилам. Порядок пар при этом не учитывается совсем.
a = {'x': 1, 'y': 2}
b = {'y': 2, 'x': 1}
print(a == b)
print(a is b)
nested1 = {'s': {'t': 'dark', 'n': 1}}
nested2 = {'s': {'n': 1, 't': 'dark'}}
print(nested1 == nested2)
True False True
Первые две строки показывают разницу между == и is: словари равны по содержимому, но это разные объекты. Третья — про глубину: вложенные словари тоже сравниваются по парам, а не по порядку, поэтому nested1 и nested2 равны, хотя ключи во внутреннем словаре записаны в разном порядке. Это работает и для списков внутри словаря — но только если сравниваются именно списки.
print([1, 2] == [2, 1])
print({'a': [1, 2]} == {'a': [2, 1]})
False False
Словари сравниваются как множества пар, а списки — как последовательности, где позиция решает всё. Словарь со списком в значении сравнивается по содержимому списка, а список со списком — по порядку элементов. Эта разница объясняет, почему два ответа API, у которых перепутан порядок элементов массива, будут признаны разными, а переставленные ключи объекта — одинаковыми.
Ещё деталь, которая однажды стоила нам получаса: числа сравниваются по значению, а не по типу. {'a': 1} == {'a': 1.0} — истина, потому что 1 == 1.0. Так что словарь, собранный из JSON, и словарь, собранный из чисел с плавающей точкой, окажутся равны, если значения совпадают numerically. А вот 1 и True — тоже равны между собой.
Три способа скопировать и разница между ними
Скопировать словарь можно тремя способами, и разница между ними — не в результате, а в глубине. dict(d) и {**d} дают новый словарь с теми же парами верхнего уровня. copy.copy(d) делает ровно то же. copy.deepcopy(d) копирует структуру насквозь — каждый вложенный словарь и список получают свои собственные копии.
src = {'a': 1}
print(dict(src))
print({**src})
print(dict(src) is src)
print({**src} is src)
{'a': 1}
{'a': 1}
False
FalseВсе четыре строки дают новый объект — is возвращает False во всех случаях. Для плоского словаря разницы между способами нет, и выбирать можно по вкусу: dict(d) выглядит как приведение типа, {**d} — как распаковка, copy.copy(d) — как явная просьба скопировать. Мы в проектах пишем dict(d): сразу видно, что это копия, а не второе имя.
Граница поверхностной копии
Поверхностная копия копирует один уровень. Вложенные словари и списки остаются общими: под ключом лежит ссылка на тот же объект, что и в оригинале. Правка верхнего уровня копии не видна в оригинале, а правка вложенного — видна в обоих.
original = {'lang': 'ru', 'meta': {'theme': 'dark'}}
copied = original.copy()
copied['lang'] = 'en'
print(original['lang'], copied['lang'])
copied['meta']['theme'] = 'light'
print(original['meta']['theme'])
ru en light
Две строки вывода показывают границу. lang изменился только в копии — верхний уровень у каждого свой. А вот theme стал 'light' и в оригинале тоже: copied['meta'] — тот же самый словарь, что и original['meta'], просто под другим именем в исходном коде. Списки внутри ведут себя так же: copied['items'].append(3) допишет элемент в оригинал.
Это не баг и не недосмотр Python: глубокое копирование стоит дорого — оно обходит всю структуру и создаёт дубликат каждого вложенного объекта. Для плоских словарей, где вложенности нет вовсе, такая работа бессмысленна. Поэтому copy() оставляют поверхностным, а решение о глубине оставляют программисту.
import copy
original = {'meta': {'theme': 'dark'}, 'items': [1, 2]}
copied = copy.deepcopy(original)
copied['meta']['theme'] = 'light'
copied['items'].append(3)
print(original)
print(copied)
{'meta': {'theme': 'dark'}, 'items': [1, 2]}
{'meta': {'theme': 'light'}, 'items': [1, 2, 3]}Оригинал не пострадал: и вложенный словарь, и список остались прежними. deepcopy обошёл структуру и создал отдельные копии всех внутренних объектов. Одна тонкость: deepcopy сохраняет и общие ссылки — если два ключа указывали на один и тот же список, в копии они снова будут указывать на один список, но уже новый. Это правильное поведение: структура копии повторяет структуру оригинала.
Сценарий: настройки перед тестом
Типичная задача, где копирование спасает. Тест проверяет, что при включённой опции меняется поведение программы. Опция лежит в глобальном словаре настроек, и тест не должен оставлять после себя мусор — иначе следующие тесты в том же процессе провалятся по причине, никак не связанной с их логикой.
import copy
settings = {'theme': 'dark', 'lang': 'ru', 'flags': {'beta': False}}
def run_test():
local = copy.deepcopy(settings)
local['theme'] = 'light'
local['flags']['beta'] = True
return local
result = run_test()
print(result['theme'], result['flags']['beta'])
print(settings['theme'], settings['flags']['beta'])
light True dark False
Здесь deepcopy не лишний. Правка верхнего уровня (local['theme']) не задела бы оригинал и при поверхностной копии, а вот local['flags']['beta'] = True изменило бы общий вложенный словарь — и следующий тест получил бы beta: True, хотя никто его не включал. Мы в одном проекте поймали именно так: два теста падали по причине, а тесты были не виноваты. С тех пор перед любой правкой настроек внутри теста делаем deepcopy.
| Способ | Что копируется | Когда брать |
|---|---|---|
| d2 = d | Ничего — второе имя | Работа под другим именем, правок не будет |
| dict(d) или {**d} | Верхний уровень | Плоский словарь, правка пар целиком |
| copy.copy(d) | Верхний уровень | То же, что dict(d), но стиль импорта |
| copy.deepcopy(d) | Вся структура насквозь | Вложенность, которую меняют независимо |
| dict(d, **extra) | Верхний уровень + новые пары | Копия сразу с изменениями |
Что дальше
Сравнение и копирование разобраны — осталось собрать финальную программу. В уроке 20 строим частотный словарь текста: чистка слов, Counter, топ-5 и ровные колонки. А если словари у вас уже есть и надо их слить — урок 14 про update и оператор |.
Сначала предскажи ответ в голове — это главный навык программиста.
a = {'x': 1, 'y': 2}
b = {'y': 2, 'x': 1}
print(a == b)
d = {'a': 1}
c = d.copy()
c['a'] = 9
print(d['a'])
d = {'s': {'t': 'dark'}}
c = d.copy()
c['s']['t'] = 'light'
print(d['s']['t'])
1. Когда два словаря равны по ==?
2. Что проверяет оператор is для словарей?
3. Что делает d.copy()?
4. Почему правка вложенного словаря через копию изменила оригинал?
5. Когда deepcopy необходим вместо copy()?
6. Что вернёт {'a': 1} == {'a': 1.0}?
7. Что произойдёт, если в цикле for по словарю добавить в него новый ключ?
8. Чем dict(d) отличается от d.copy()?
Даны a = {'a': 1, 'b': 2} и b = {'b': 2, 'a': 1}. Выведи результат сравнения a == b. Затем сделай c = a.copy(), поменяй в c значение 'b' на 9 и выведи a и c на отдельных строках — оригинал должен остаться со старым значением.
Как сравнить два словаря в Python?
Оператором ==: он вернёт True, если у словарей одинаковые пары ключ — значение. Порядок пар при сравнении не важен. Для проверки «это один и тот же объект или копия» есть оператор is — он про идентичность, а не про содержимое.
Урок 2: создание словарейЧем copy() отличается от deepcopy?
copy() копирует только верхний уровень: вложенные словари остаются общими с оригиналом. copy.deepcopy() из модуля copy копирует структуру насквозь — правки копии не видны в оригинале ни на одном уровне.
Почему изменение копии словаря меняет оригинал?
Скорее всего правка шла по вложенному уровню: copy() поверхностный, и под ключами обе переменные держат одни и те же вложенные объекты. Для независимых правок вложенности нужна copy.deepcopy(). Если меняется и верхний уровень — проверьте, не присваивание ли это: d2 = d копии не создаёт.
Урок 14: update меняет объектКак скопировать словарь с вложенными словарями?
Функцией copy.deepcopy: import copy; copied = copy.deepcopy(original). Каждый вложенный словарь и список получит собственную копию, и любые правки копии останутся в копии. Для плоских словарей хватает обычного copy() — deepcopy дороже по времени и памяти.
Почему два словаря с разным порядком ключей равны?
Оператор == сравнивает словари как множества пар: важно, какие пары есть, а не в каком порядке они записаны. Это верно и для вложенных словарей — они тоже сравниваются по парам. А вот списки сравниваются по порядку, поэтому {'a': [1, 2]} и {'a': [2, 1]} не равны.
Как проверить, что два словаря — один объект или две копии?
Оператором is: он проверяет идентичность объектов, а не содержимое. Для копии, сделанной через dict(d) или copy(), is вернёт False, а == вернёт True. Помните: у мелких чисел и коротких строк Python переиспользует объекты, так что is для отдельных значений может дать True — но для словарей как контейнеров это не так.
Что будет, если изменить вложенный словарь в копии, сделанной через copy()?
Изменение дойдёт до оригинала: copy() копирует только верхний уровень, а под ключом остаётся ссылка на тот же вложенный словарь. Видна новая обёртка и общая начинка. Для независимых вложенных правок нужен copy.deepcopy.
Почему нельзя менять словарь во время перебора по нему?
Потому что хеш-таблица рассчитана на фиксированный размер: добавили ключ — таблица перестроилась, и Python падает с RuntimeError: dictionary changed size during iteration. Нужные ключи добавляют после цикла или собирают заранее отдельным списком.
Понравился урок? Сошлитесь на него
«Два словаря равны, когда у них одинаковые пары, а порядок пар при сравнении не важен.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Словари · Урок 18
Ключи: что можно и что нельзя
Ключом бывают строки, числа и кортежи, но не список: TypeError: unhashable type объясняется устройством словаря, а frozenset спасает, когда нужен набор.
Словари · Урок 20
Проект: частотный словарь текста
Финальный проект: частотный словарь текста — режем текст на слова, чистим, отбрасываем стоп-слова, считаем через Counter и выводим топ-5 ровными колонками.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Исключения · Урок 11
KeyError в Python: ошибки словарей
Словарь не нашёл ключ — и честно сказал об этим KeyError. Разбираем объект ошибки, читаем из него ключ и выбираем между get, in и честным исключением.
keyerror словарь pythonpython словарь нет ключа
Datetime · Урок 7
Сравнение и сортировка дат
Даты сравниваются как числа — и потому сортируются, выбираются min и max и становятся ключами словаря. Разбираем операторы и честную сортировку без строковых сюрпризов.
сравнение дат pythonpython сравнить даты
Словари · Урок 6
Удаление: del, pop и popitem
Четыре способа убрать данные из словаря: del молча удаляет пару, pop отдаёт значение, popitem снимает последнюю пару, clear чистит всё. Плюс разбор, почему удалять во время обхода нельзя.
python dict удалитьудалить элемент из словаря python