На этом шаге мы рассмотрим способы объединения словарей.
Вы когда-нибудь конструировали систему конфигурации для одной из ваших программ Python? В таких системах принято принимать структуру данных с параметрами конфигурации, заданными по умолчанию, а затем предоставлять возможность селективно переопределять эти параметры на основе вводимых пользователем данных или некоторого другого источника конфигурации.
Словари нередко используются в качестве базовой структуры данных для представления ключей и значений конфигурации. И поэтому часто бывает необходим способ объединения, или слияния (merge), принятых по умолчанию параметров конфигурации с пользовательскими переопределениями в один-единственный словарь с окончательными значениями конфигурации.
Или, обобщая: иногда вам нужен способ объединить два или более словаря в один, чтобы результирующий словарь содержал комбинацию ключей и значений исходных словарей.
На этом шаге мы покажем несколько способов сделать это. Сначала посмотрим на простой пример, чтобы можно было что-то обсуждать. Предположим, что у вас имеется два исходных словаря:
>>> xs = {'a': 1, 'b': 2}
>>> ys = {'b': 3, 'c': 4}
И вы хотите создать новый словарь zs, который содержит все ключи и значения xs и все ключи и значения ys. Кроме того, если вы внимательно прочли этот пример, то вы заметили, что строка 'b' появляется в качестве ключа в обоих словарях, - нам также придется продумать стратегию разрешения конфликтов для повторяющихся ключей.
В Python классическое решение задачи "слияния многочисленных словарей" состоит в том, чтобы использовать встроенный в словарь метод update():
>>> zs = {}
>>> zs.update(xs)
>>> zs.update(ys)
Если вам любопытно, то наивная реализация функции update() могла бы выглядеть примерно следующим образом. Мы просто перебираем в цикле все элементы словаря с правой стороны и добавляем каждую пару ключ-значение в словарь с левой стороны, по ходу переписывая существующие ключи:
def update(dict1, dict2): for key, value in dict2.items(): dict1[key] = value
В результате мы получим новый словарь zs, который теперь содержит ключи, определенные в xs и ys:
>>> zs
{'c': 4, 'a': 1, 'b': 3}
Вы также увидите, что порядок, в котором мы вызываем update(), определяет то, как будут разрешаться конфликты. Выигрывает последнее обновление, и повторяющийся ключ 'b' ассоциируется со значением 3, которое поступило из ys, то есть второго исходного словаря.
Разумеется, вы можете расширить эту цепочку вызовов update() настолько, насколько захотите, для того, чтобы объединить любое количество словарей в один словарь. Такое практическое и удобочитаемое решение работает в Python 2 и в Python 3.
Еще один прием, который работает в Python 2 и в Python 3, использует встроенную функцию dict() совместно с оператором ** для "распаковки" объектов:
>>> zs = dict(xs, **ys) >>> zs {'c': 4, 'a': 1, 'b': 3}
Однако, как и в случае с повторными вызовами update(), этот подход работает только для слияния исключительно двух словарей и не может быть обобщен для объединения произвольного количества словарей за один шаг.
Начинания с Python 3.5, оператор ** стал более гибким.
См. PEP 448 "Дополнительные обобщения распаковки": https://peps.python.org/pep-0448/.
Поэтому в Python 3.5+ есть еще один - и, пожалуй, более приятный - способ объединения произвольного количества словарей:
>>> zs = {**xs, **ys}
У этого выражения в точности такой же результат, что и у цепочки вызовов update(). Ключи и значения задаются в порядке слева направо, поэтому мы получаем ту же самую стратегию разрешения конфликтов: правая сторона имеет приоритет, а значение в ys переопределяет любое существующее значение под тем же самым ключом в xs. Это станет понятным, когда мы посмотрим на словарь, который является результатом этой операции слияния:
>>> zs
{'c': 4, 'a': 1, 'b': 3}
Новый вариант этой синтаксической конструкции по-прежнему остается достаточно удобочитаемой. Всегда приходится находить равновесие между многословностью и краткостью, сохраняя программный код максимально удобочитаемым и легким в сопровождении.
Отметим, что при использовании оператора ** операция слияния выполняется быстрее, чем при использовании цепочки вызовов update(), что является еще одним преимуществом.
На следующем шаге мы подитожим изученный материал.