Объединение словарей: update и оператор |
Два словаря в один: update переписывает на месте, оператор | собирает новый словарь, слои настроек ставятся по приоритету, а при конфликте ключей побеждает правый.
Редакция Питоники
Настройки программы живут слоями: сначала значения по умолчанию, поверх — правки пользователя. Склеить два словаря в один — постоянная задача, и у неё два идиоматических решения: метод update, работающий с ранних версий Python, и оператор |, появившийся в Python 3.9. Разница между ними — судьба левого словаря.
update: влить второй словарь
settings = {'theme': 'dark', 'lang': 'ru'}
settings.update({'lang': 'en', 'ads': False})
print(settings)
{'theme': 'dark', 'lang': 'en', 'ads': False}update вливает второй словарь в первый на месте: пара 'lang' перезаписалась, 'ads' добавилась. Метод меняет сам объект — словарь settings после вызова уже другой. Это удобно, когда левый словарь и есть накопитель: собираем всё в один settings, как пары в уроке 5, только пачкой. Аргументом бывает и другой словарь, и список пар: settings.update([('lang', 'en'), ('ads', False)]).
Оператор |: новый словарь
base = {'theme': 'dark', 'lang': 'ru'}
user = {'lang': 'en', 'ads': False}
merged = base | user
print(merged)
print(base)
{'theme': 'dark', 'lang': 'en', 'ads': False}
{'theme': 'dark', 'lang': 'ru'}Оператор | собирает третий словарь: сначала пары левого, затем правого, и оба исходных остаются целы. Для обновления на месте есть форма |=: settings |= user делает то же, что settings.update(user). Оператор появился в Python 3.9; на старых версиях тот же результат давала запись {**base, **user} — распаковка двух словарей в один.
Конфликт ключей: побеждает правый
defaults = {'volume': 5, 'theme': 'dark'}
custom = {'theme': 'light'}
print(defaults | custom)
print(custom | defaults)
{'volume': 5, 'theme': 'light'}
{'theme': 'dark', 'volume': 5}Один и тот же набор словарей, два разных ответа. При конфликте ключей побеждает правый словарь: позднее значение перезаписывает раннее, как свежая правка поверх черновика. Обратите внимание на порядок пар: ключ 'theme' остался на позиции первой вставки, поменялось только значение. Поэтому правки пользователя ставят справа — они и должны выигрывать. Цепочки работают и длиннее: a | b | c соберёт три словаря, и при каждом конфликте снова победит правый.
Три способа объединить и когда какой выбрать
Способы три, и разница между ними не в результате, а в судьбе исходных объектов. update меняет левый словарь на месте и возвращает None. Оператор | собирает новый словарь, оставляя оба исходника целыми. Распаковка {**a, **b} даёт тот же новый словарь, но работает и в Python старше 3.9 — иногда это единственный доступный вариант в чужом коде.
defaults = {'theme': 'dark', 'lang': 'ru'}
user = {'lang': 'en', 'ads': False}
print(defaults | user)
print({**defaults, **user})
merged = dict(defaults)
merged.update(user)
print(merged)
{'theme': 'dark', 'lang': 'en', 'ads': False}
{'theme': 'dark', 'lang': 'en', 'ads': False}
{'theme': 'dark', 'lang': 'en', 'ads': False}Все три строки напечатали одинаковый словарь — и это лучший признак того, что выбор не имеет отношения к результату, а только к тому, что вы собираетесь делать с исходниками дальше. Если defaults больше нигде не нужен — берите update и не заводите лишнюю переменную. Если умолчания нужны нетронутыми (тест против production, откат конфига) — берите |. Распаковку оставьте для кода, который должен работать на Python 3.8 и старше.
| Способ | Что происходит с левым | Когда брать |
|---|---|---|
| a.update(b) | Меняет на месте, возвращает None | Левый словарь — накопитель, исходник не нужен |
| a | b | Не трогает, собирает новый | Исходники нужны целыми |
| a |= b | Меняет на месте, синтаксис оператора | То же, что update, но читается как операция |
| {a, b} | Не трогает, собирает новый | Нужен Python до 3.9 или обход ограничений |
| ChainMap(a, b) | Не трогает, смотрит по слоям | Слои должны оставаться раздельно |
Слои настроек: умолчания, проект, пользователь
Реальная задача, где объединение нужно по-настоящему, — конфигурация приложения. Есть три слоя: умолчания разработчика, настройки проекта и правки конкретного пользователя. Каждый слой задаёт часть ключей, и правило простое: чья правка свежее, тот и побеждает. Порядок слоёв задаёт приоритет, и менять его местами нельзя.
base = {'theme': 'dark', 'lang': 'ru', 'volume': 5}
project = {'lang': 'en', 'volume': 8}
user_layer = {'volume': 11}
settings = base | project | user_layer
print(settings)
print(base)
{'theme': 'dark', 'lang': 'en', 'volume': 11}
{'theme': 'dark', 'lang': 'ru', 'volume': 5}Читаем результат по слоям. theme не переопределял никто — осталось 'dark' из базового слоя. lang переопределил только проектный слой — стало 'en'. volume переопределяли все трое по очереди, и последним встал пользовательский слой — 11. Ключ theme остался на позиции первой вставки, хотя пришёл из самого первого слоя: порядок пар в словаре определяет вставка, а не «кто победил».
Обратите внимание на вторую строку вывода: base не изменился. Именно ради таких слоёв и нужен оператор | — базовый словарь потом ещё сравнивают с итоговым, чтобы показать в логе, какие настройки переопределены. Если бы мы объединили через update, базовый слой переписался бы, и сравнивать было бы не с чем. Мы в одном проекте так и сделали — и лог показывал «переопределено: theme, lang», хотя theme никто не трогал.
Глубокое объединение: почему | меняет вложенный словарь целиком
Вот место, где объединение словарей предаёт. Если под ключом лежит не число, а вложенный словарь, оператор | заменяет его целиком, а не дополняет. Пользовательские правки внутри вложенной структуры не сливаются с базовыми — они их стирают. Ошибка выглядит безобидно: в результате просто не хватает пар, которых «раньше вроде был».
defaults = {'ui': {'theme': 'dark', 'lang': 'ru'}, 'volume': 5}
custom = {'ui': {'lang': 'en'}}
print(defaults | custom)
{'ui': {'lang': 'en'}, 'volume': 5}Смотрите, что произошло с ключом ui. Базовый вложенный словарь содержал два поля — theme и lang. После объединения в нём осталось одно поле: lang. Настройка темы потерялась, потому что оператор заменяет значение по ключу, а значение — это весь словарь {'theme': 'dark', 'lang': 'ru'}. Никакого слияния на глубину не происходит: | работает только на верхнем уровне.
Если глубина действительно нужна, слияние приходится писать руками. Функция ниже различает два случая: если и старое, и новое значения — словари, сливаем их рекурсивно; если нет — новое значение просто заменяет старое.
def deep_merge(base, extra):
result = dict(base)
for key, value in extra.items():
if isinstance(value, dict) and isinstance(result.get(key), dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
defaults = {'ui': {'theme': 'dark', 'lang': 'ru'}, 'volume': 5}
custom = {'ui': {'lang': 'en'}}
print(deep_merge(defaults, custom))
print(defaults)
{'ui': {'theme': 'dark', 'lang': 'en'}, 'volume': 5}
{'ui': {'theme': 'dark', 'lang': 'ru'}, 'volume': 5}Теперь theme на месте: тема не была переопределена, и глубокое слияние её сохранило. Функция возвращает новый словарь и исходный не трогает — так же, как |. Оговорка честная: такая функция годится для конфигов и подобных плоских по глубине структур, но для произвольных данных она лишняя. Словарь в значении словаря — это уже данные, а не настройки, и тянуть вниз рекурсию по ним обычно не нужно.
update принимает не только словарь
Метод update умеет больше, чем кажется: он берёт любой итерируемый объект, состоящий из пар. Список кортежей подходит, как и список списков — важно только, чтобы элементы распаковывались в два значения. Второй вариант — именованные аргументы: update(b=2, c=3) добавит ключи 'b' и 'c'. Работает и с другим словарём, и с Counter, и с любым Mapping.
d = {'a': 1}
d.update([('b', 2), ('c', 3)])
print(d)
e = {'a': 1}
e.update(b=2, c=3)
print(e)
{'a': 1, 'b': 2, 'c': 3}
{'a': 1, 'b': 2, 'c': 3}Обе формы дают один результат, и это удобно: когда пары приходят из CSV-строки или из списка, разбирать их в словарь вручную не нужно — update сделает это сам. Оговорка про именованные аргументы: ключ при этом всегда строка, потому что имена в вызове функции записываются текстом. Числовой ключ через update не добавить — только через список пар.
Что дальше
Словари умеют объединяться и сортироваться — осталось собрать новый словарь из данных, которые уже есть. В уроке 15 разбираем dict comprehension: {k: v for k, v in ...} собирает словарь за один проход, в том числе инвертирует его. А если на вход приходит текст с внешнего сервиса, сначала прочитайте урок про JSON — там словарь и текстовый формат превращаются друг в друга.
Сначала предскажи ответ в голове — это главный навык программиста.
s = {'a': 1}
s.update({'b': 2})
print(s)
base = {'x': 1}
user = {'x': 9, 'y': 2}
merged = base | user
print(merged)
print(base)
s = {'theme': 'dark'}
s |= {'lang': 'en'}
print(s)
1. Что делает settings.update(extra)?
2. Чем a | b отличается от a.update(b)?
3. При конфликте ключей кто побеждает в a | b?
4. Что делает a |= b?
5. На какой версии Python появился оператор | для словарей?
6. Что произойдёт при объединении словарей, если под ключом лежит вложенный словарь?
7. Как объединить три слоя настроек так, чтобы пользовательские правки имели приоритет?
8. Что вернёт {'a': 1}.update([('b', 2)])?
Даны defaults = {'volume': 5, 'lang': 'ru'} и custom = {'lang': 'en'}. Собери объединённый словарь settings через оператор | и выведи его, затем выведи defaults — он должен остаться прежним.
Как объединить два словаря в Python?
Оператором |: merged = base | user соберёт новый словарь из пар обоих (Python 3.9+). Методом update: base.update(user) вольёт user в base на месте. На старых версиях работает распаковка {base, user}.
Урок 13: сортировка словаряКакое значение выживает при конфликте ключей?
Значение правого словаря: при объединении поздние пары перезаписывают ранние. Ключ при этом остаётся на позиции первой вставки — меняется только значение. Поэтому правки пользователя ставят справа от умолчаний.
Чем | лучше update?
Ни тем, что быстрее, ни тем, что короче, — разница в судьбе исходников. Оператор | не меняет ни один из них и возвращает новый словарь: удобно, когда базовый слой настроек надо ещё сравнить с итоговым. update меняет левый словарь на месте и возвращает None: удобно, когда левый словарь и есть накопитель. Если исходник больше не нужен, экономия в одну присваивание перевешивает.
Как объединить словари в старых версиях Python?
Распаковкой: {base, user} — правые пары тоже побеждают при конфликте. Сам оператор | появился только в Python 3.9, update же работает во всех версиях.
Как объединить три словаря сразу?
Цепочкой: base | project | user. При каждом шаге побеждает правый, поэтому порядок в цепочке — это порядок приоритета: пользовательские правки ставят последними. update так не умеет: он работает с одним источником за вызов, хотя список пар принять может.
Урок 5: добавление и перезаписьПочему после объединения пропало поле во вложенном словаре?
Потому что update и оператор | работают только на верхнем уровне: значение по ключу заменяется целиком. Если под ключом 'ui' лежал словарь {'theme': 'dark', 'lang': 'ru'}, а пришёл {'lang': 'en'}, в результате останется только {'lang': 'en'}. Для слияния по глубине нужна своя функция с рекурсией.
Как обновить словарь из списка пар?
Методом update: d.update([('b', 2), ('c', 3)]). Он принимает любой итерируемый объект из пар — список кортежей, список списков, даже Counter. Есть и вторая форма: именованные аргументы d.update(b=2, c=3), но там ключ всегда строка, потому что имена в вызове пишутся текстом.
Можно ли объединить словарь со списком?
Нет: оператор | требует с обеих сторон словари, и {'a': 1} | [('b', 2)] падает с TypeError: unsupported operand type(s) for |: 'dict' and 'list'. Список пар сначала превращают в словарь: dict([('b', 2)]), а потом уже объединяют.
Понравился урок? Сошлитесь на него
«При конфликте ключей побеждает правый словарь: позднее значение перезаписывает раннее, как свежая правка поверх черновика.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Словари · Урок 13
Сортировка словаря по ключам и значениям
Словарь нельзя отсортировать на месте — но можно получить отсортированный: sorted по ключам, key=lambda по значениям, вторичный ключ кортежем, top-N срезом и dict(sorted(...)) обратно.
Словари · Урок 15
dict comprehension: словарь за строку
Dict comprehension собирает словарь за одну строку: квадраты чисел, цены со скидкой, фильтр по условию, инверсия ключей и значений — и словарь из списка через enumerate.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Множества · Урок 6
Объединение множеств в Python: оператор | и union
Гости субботы плюс гости воскресенья — но каждый посчитан один раз. Оператор | и метод union собирают два множества в одно без дубликатов.
python объединение множествpython set union
Исключения · Урок 11
KeyError в Python: ошибки словарей
Словарь не нашёл ключ — и честно сказал об этим KeyError. Разбираем объект ошибки, читаем из него ключ и выбираем между get, in и честным исключением.
keyerror словарь pythonpython словарь нет ключа
Словари · Урок 9
Вложенные словари: словарь внутри словаря
Каталог товаров, где у каждого товара свой словарь с ценой и остатком: доступ по цепочке ключей, запись во вложенные поля, перебор и разбор того, почему два уровня вложенности — это предел.
вложенные словари pythondict в dict python