Подтвердить что ты не робот

Объединить два списка словарей на одном ключе

Учитывая n списки с m словарями в качестве своих элементов, я хотел бы создать новый список с объединенным набором словарей. Гарантируется, что каждый словарь имеет ключ под названием "index", но может иметь произвольный набор ключей за его пределами. Неиндексные ключи никогда не будут перекрываться между списками. Например, представьте себе следующие два списка:

l1 = [{"index":1, "b":2}, {"index":2, "b":3}, {"index":3, "green":"eggs"}]
l2 = [{"index":1, "c":4}, {"index":2, "c":5}]

("b" никогда не появится в l2, так как он появился в l1, и аналогичным образом "c" никогда не появится в l1, поскольку он появился в l2)

Я хотел бы создать объединенный список:

l3 = [{"index":1, "b":2, "c":4}, 
      {"index":2, "b":3, "c":5}, 
      {"index":3, "green":"eggs"}]

Каков наиболее эффективный способ сделать это в Python?

4b9b3361

Ответ 1

from collections import defaultdict

l1 = [{"index":1, "b":2}, {"index":2, "b":3}, {"index":3, "green":"eggs"}]
l2 = [{"index":1, "c":4}, {"index":2, "c":5}]

d = defaultdict(dict)
for l in (l1, l2):
    for elem in l:
        d[elem['index']].update(elem)
l3 = d.values()

# l3 is now:

[{'b': 2, 'c': 4, 'index': 1},
 {'b': 3, 'c': 5, 'index': 2},
 {'green': 'eggs', 'index': 3}]

EDIT: поскольку l3 не гарантируется сортировка (.values() возвращает элементы без определенного порядка), вы можете сделать, как @user560833 предлагает:

from operator import itemgetter

...

l3 = sorted(d.values(), key=itemgetter("index"))

Ответ 2

Вот однострочный, который делает это:

[dict(sum([z.items() for z in z2],[])) for z2 in [[x3 for x3 in l1+l2 if x3['index']==key] for key in set([x1['index'] for x1 in l1]+[x2['index'] for x2 in l2])]]

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

Развертывание однострочного слоя:

[
    dict(sum([z.items() for z in z2],[])) 
    for z2 in [
        [
            x3 for x3 in l1+l2 if x3['index']==key
        ] for key in set(
            [x1['index'] for x1 in l1]+[x2['index'] for x2 in l2]
        )
    ]
]

Устанавливаемое выражение на 6-й строке получает все уникальные значения индекса из обоих списков. Сопоставление списка вокруг этого (строки 3-9) создает список списков, где каждый внутренний список представляет собой комбинированный список словарей для этого индекса/ключа с определенным значением индекса. Самое внешнее понимание списка создает единый список кортежей для каждого ключа и преобразует его обратно в список словарей.

Ответ 3

В Python 3.5 или выше, вы можете объединить словари в одном выражении.

Таким образом, для Python 3.5 или выше, быстрое решение будет:

from itertools import zip_longest

l3 = [{**u, **v} for u, v in zip_longest(l1, l2, fillvalue={})]

print(l3)
#[
#    {'index': 1, 'b': 2, 'c': 4}, 
#    {'index': 2, 'b': 3, 'c': 5}, 
#    {'index': 3, 'green': 'eggs'}
#]

Однако, если бы два списка были одинакового размера, вы могли бы просто использовать zip:

l3 = [{**u, **v} for u, v in zip(l1, l2)]

Примечание. Предполагается, что списки сортируются одинаково по index, который, как указано в OP, не соответствует действительности.

Чтобы обобщить для этого случая, одним из способов является создание пользовательской функции типа zip-long-long, которая возвращает значения из двух списков, только если они совпадают по ключу.

Например:

def sortedZipLongest(l1, l2, key, fillvalue={}):  
    l1 = iter(l1)
    l2 = iter(l2)
    u = next(l1, None)
    v = next(l2, None)

    while (u is not None) or (v is not None):

        if u is None:
            yield fillvalue, v
            v = next(l2, None)
        elif v is None:
            yield u, fillvalue
            u = next(l1, None)
        elif u.get(key) == v.get(key):
            yield u, v
            u = next(l1, None)
            v = next(l2, None)
        elif u.get(key) < v.get(key):
            yield u, fillvalue
            u = next(l1, None)
        else:
            yield fillvalue, v
            v = next(l2, None)

Теперь, если у вас были следующие списки:

l1 = [{"index":1, "b":2}, {"index":2, "b":3}, {"index":3, "green":"eggs"}, 
      {"index":4, "b": 4}]
l2 = [{"index":1, "c":4}, {"index":2, "c":5}, {"index":0, "green": "ham"}, 
      {"index":4, "green": "ham"}]

Использование функции sortedZipLongest вместо itertools.zip_longest:

l3 = [{**u, **v} for u, v in sortedZipLongest(l1, l2, key="index", fillvalue={})]
print(l3)
#[{'index': 1, 'b': 2, 'c': 4},
# {'index': 2, 'b': 3, 'c': 5},
# {'index': 0, 'green': 'ham'},
# {'index': 3, 'green': 'eggs'},
# {'index': 4, 'b': 4, 'green': 'ham'}]

Принимая во внимание, что оригинальный метод дал бы неправильный ответ:

l3 = [{**u, **v} for u, v in zip_longest(l1, l2, fillvalue={})]
print(l3)
#[{'index': 1, 'b': 2, 'c': 4},
# {'index': 2, 'b': 3, 'c': 5},
# {'index': 0, 'green': 'ham'},
# {'index': 4, 'b': 4, 'green': 'ham'}]