Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 15 мин 110 XP

Объединение словарей: 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)
Проверь себя
0 / 8

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)])?

Карточки терминов
Запомнено: 0 / 10
Практика

Даны defaults = {'volume': 5, 'lang': 'ru'} и custom = {'lang': 'en'}. Собери объединённый словарь settings через оператор | и выведи его, затем выведи defaults — он должен остаться прежним.

practice.py
Вопросы и ответы по уроку

Как объединить два словаря в 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.