Объединить/соединить списки словарей на основе общего значения в Python

У меня есть два списка словарей (возвращаемых в виде запросов Django). Каждый словарь имеет значение ID. Я хотел бы объединить их в один список словарей, основанный на значении идентификатора.

Например:

list_a = [{'user__name': u'Joe', 'user__id': 1},
          {'user__name': u'Bob', 'user__id': 3}]
list_b = [{'hours_worked': 25, 'user__id': 3},
          {'hours_worked': 40, 'user__id': 1}]

и мне нужна функция:

list_c = [{'user__name': u'Joe', 'user__id': 1, 'hours_worked': 40},
          {'user__name': u'Bob', 'user__id': 3, 'hours_worked': 25}]

Дополнительные примечания:

  • Идентификаторы в списках могут быть не в том же порядке (как в примере выше).
  • В списках, вероятно, будет одинаковое количество элементов, но я хочу учесть этот параметр, если они не сохраняют все значения из list_a (по существу list_a OUTER JOIN list_b USING user__id).
  • Я пытался сделать это в SQL, но это невозможно, поскольку некоторые из значений являются агрегатами на основе некоторых исключений.
  • Безопасно предположить, что в каждом списке будет использоваться не более одного словаря с тем же user__id из-за использованных запросов к базе данных.

Большое спасибо за ваше время.

Ответ 1

Я бы использовал itertools.groupby для группировки элементов:

lst = sorted(itertools.chain(list_a,list_b), key=lambda x:x['user__id'])
list_c = []
for k,v in itertools.groupby(lst, key=lambda x:x['user__id']):
    d = {}
    for dct in v:
        d.update(dct)
    list_c.append(d)
    #could also do:
    #list_c.append( dict(itertools.chain.from_iterable(dct.items() for dct in v)) )
    #although that might be a little harder to read.

Если у вас есть отвращение к функциям lambda, вы всегда можете использовать operator.itemgetter('user__id'). (это, вероятно, немного более эффективно)

Чтобы немного размять лямбда /itemgetter, обратите внимание, что:

def foo(x):
    return x['user__id']

- одно и то же *, как одно из следующих:

foo = operator.itemgetter('user__id')
foo = lambda x: x['user__id']

* Есть несколько отличий, но они не важны для этой проблемы.

Ответ 2

from collections import defaultdict
from itertools import chain

list_a = [{'user__name': u'Joe', 'user__id': 1},
      {'user__name': u'Bob', 'user__id': 3}]
list_b = [{'hours_worked': 25, 'user__id': 3},
      {'hours_worked': 40, 'user__id': 1}]

collector = defaultdict(dict)

for collectible in chain(list_a, list_b):
    collector[collectible['user__id']].update(collectible.iteritems())

list_c = list(collector.itervalues())

Как вы можете видеть, это просто использует другой dict для слияния существующих dicts. Трюк с defaultdict заключается в том, что он извлекает трудность создания dict для новой записи.

Нет необходимости группировать или сортировать эти входы. Этот дикт заботится обо всем этом.

Поистине пуленепробиваемое решение поймает потенциальную ключевую ошибку, если на входе нет ключа "user__id", или используйте значение по умолчанию, чтобы собрать все диктофоны без такого ключа.