在单个键上连接两个字典列表

49 投票
3 回答
45994 浏览
提问于 2025-04-16 14:49

假设你有 n 个列表,每个列表里有 m 个字典。我想要生成一个新的列表,这个列表里的字典是合并后的结果。每个字典里都有一个叫做 "index" 的键,但除此之外,它们可以有其他任意的键。而且这些非 "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 中,最有效的方法是什么呢?

3 个回答

1

这里有一行代码可以做到这一点:

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

第六行的集合表达式会从两个列表中获取所有独特的索引值。第三到第九行的列表推导式则会生成一个列表,这个列表里面每个小列表都是根据特定的索引值组合的字典。最外层的列表推导式会把每个键的元组对组合成一个单一的列表,并且再转换回字典列表。

19

在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的顺序排列的,而这在一般情况下并不成立

为了适应这种情况,可以创建一个自定义的zip-longest类型的函数,这个函数只在两个列表的某个键匹配时才返回值。

比如说:

def sortedZipLongest(l1, l2, key, fillvalue={}):  
    l1 = iter(sorted(l1, key=lambda x: x[key]))
    l2 = iter(sorted(l2, key=lambda x: x[key]))
    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': 0, 'green': 'ham'},
# {'index': 1, 'b': 2, 'c': 4},
# {'index': 2, 'b': 3, 'c': 5},
# {'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'}]
59
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}]
from operator import itemgetter

...

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

编辑: 因为 l3 并不一定是排好序的(.values() 返回的项目没有特定的顺序),你可以按照 @user560833 的建议来做:

撰写回答