如何合并字典并收集匹配键的值?

153

我有多个类似以下的字典(或键值对序列):

d1 = {key1: x1, key2: y1}
d2 = {key1: x2, key2: y2}

如何高效地将类似这样的结果作为一个新字典获取?

d = {key1: (x1, x2), key2: (y1, y2)}

参见:如何在Python中创建具有重复键的字典?


4
可以假设每个键在所有字典中都存在吗? - Björn Pollex
嗨,Space_C0wb0y,是的,所有字典中都存在这些键。 - Salil
非常重要的是要指定所有字典是否具有相同的键。 - yugr
18个回答

127

这里有一个通用的解决方案,可以处理任意数量的字典,包括只在某些字典中存在键的情况:

from collections import defaultdict

d1 = {1: 2, 3: 4}
d2 = {1: 6, 3: 7}

dd = defaultdict(list)

for d in (d1, d2): # you can list as many input dicts as you want here
    for key, value in d.items():
        dd[key].append(value)
    
print(dd) # result: defaultdict(<type 'list'>, {1: [2, 6], 3: [4, 7]})

1
我认为原帖作者想要元组形式的值,而不是列表形式。 - user225312
6
@A A:这真的很重要吗?在多个输入字典的更一般情况下,如果某些键并非到处都存在,建立元组会更加棘手,我个人认为。 - Eli Bendersky
1
你可能想将defaultdict转换为普通的dict,这样对于不存在的键等就可以具有普通的dict行为:dd = dict(dd) - Ned Deily
1
@Eli:不,这并不重要,但我只是试图根据原帖作者的需求来制定方案,并希望您能提供元组的解决方案 :-) - user225312
@Eli,我的属性对于不同的字典是不同的,即d1[key1].x1attrib和d2[key1].x2attrib。 - Salil
显示剩余4条评论

65

假设所有的字典中都始终包含所有的键:

ds = [d1, d2]
d = {}
for k in d1.iterkeys():
    d[k] = tuple(d[k] for d in ds)

注意:在Python 3.x中,请使用以下代码:

ds = [d1, d2]
d = {}
for k in d1.keys():
  d[k] = tuple(d[k] for d in ds)

如果字典包含NumPy数组:

ds = [d1, d2]
d = {}
for k in d1.keys():
  d[k] = np.concatenate(list(d[k] for d in ds))

4
我认为只需要使用 "for k in d1" 就可以了。 - Salil
并在d[k]的位置上使用d.get(k, None) - tahir
1
@tahir 这意味着字典具有不匹配的键,因此迭代 d1 不正确(它可能会忽略其他字典中的键)。 - yugr
1
对于Python 3用户:d1.iterkeys() = d1.items() - Riley

13

这个函数可以合并两个字典,即使这两个字典的键不同:

def combine_dict(d1, d2):
    return {
        k: tuple(d[k] for d in (d1, d2) if k in d)
        for k in set(d1.keys()) | set(d2.keys())
    }

例子:

d1 = {
    'a': 1,
    'b': 2,
}
d2 = {
    'b': 'boat',
    'c': 'car',
    'd': 'donkey',
}
combine_dict(d1, d2)
# Returns: {
#    'a': (1,),
#    'b': (2, 'boat'),
#    'c': ('car',),
#    'd': ('donkey'),
# }

如果参数相同或者参数数量不同怎么办?例如d1 = { 'a': [1,2,3], 'b': 2, } d2` = { 'b': 'boat', 'c': 'car', 'a': [1,3] } - KyluAce
@KyluAce 在你的情况下,combine_dict(d1, d2) 将返回 {'b': (2, 'boat'), 'c': ('car',), 'a': ([1, 2, 3], [1, 3])} - Flux

5
dict1 = {'m': 2, 'n': 4}
dict2 = {'n': 3, 'm': 1}

确保键值按照相同的顺序:
dict2_sorted = {i:dict2[i] for i in dict1.keys()}

keys = dict1.keys()
values = zip(dict1.values(), dict2_sorted.values())
dictionary = dict(zip(keys, values))

给出:
{'m': (2, 1), 'n': (4, 3)}

2
values() 中元素的顺序未定义,因此您可能会合并来自不相关键的值。 - yugr
我刚刚应用了更改,现在可以捕获您的反馈。 - Mahdi Ghelichi
我认为更改不会解决问题。您需要使用sorted(d.items())sorted(d.keys())来获得可预测的结果。 - yugr
你能给一个反例吗? dict2_sorted是Python中的一个已排序字典! - Mahdi Ghelichi
3
我对此进行了一些小型研究。在Python的最新版本(3.6+)中,迭代顺序开始匹配插入顺序(例如,请参见此处),这使得您的代码可以通过。但是,这被认为是一个不应该依赖的实现细节。我的第二个例子(请参见此处)在使用旧版Python 3.4的onlinegdb中可靠地失败。其他在线解释器使用更新的Python,因此无法在那里重现问题。 - yugr
显示剩余9条评论

4
这里有一种方法,即使两个字典的键不相同也可以使用:
d1 = {'a':'test','b':'btest','d':'dreg'}
d2 = {'a':'cool','b':'main','c':'clear'}

d = {}

for key in set(list(d1.keys()) + list(d2.keys())):
    try:
        d.setdefault(key,[]).append(d1[key])        
    except KeyError:
        pass

    try:
        d.setdefault(key,[]).append(d2[key])          
    except KeyError:
        pass

print(d)

这将生成以下输入:
{'a': ['test', 'cool'], 'c': ['clear'], 'b': ['btest', 'main'], 'd': ['dreg']}

1
在答案中,可以将 set(d1.keys() + d2.keys()) 更改为 set(list(d1.keys()) + list(d2.keys())) 吗?(适用于 Python 3.x)否则会抛出 TypeError: unsupported operand type(s) for +: 'dict_keys' and 'dict_keys' 错误。 - R4444

4
如果您只有d1和d2,
from collections import defaultdict

d = defaultdict(list)
for a, b in d1.items() + d2.items():
    d[a].append(b)

1
.items() 的结果在 3.x 中无法添加。 - Karl Knechtel

2

使用预计算密钥

def merge(dicts):
    # First, figure out which keys are present.
    keys = set().union(*dicts)
    # Build a dict with those keys, using a list comprehension to
    # pull the values from the source dicts.
    return {
        k: [d[k] for d in dicts if k in d]
        for k in keys
    }

这本质上是Flux的答案,为输入字典列表进行了概括。
通过对所有源字典中的键进行set联合,set().union技巧起作用。在一个空集上使用setunion方法(我们从一个空集开始)可以接受任意数量的参数,并将每个输入与原始集合进行联合;它还可以接受其他可迭代对象(不需要其他set作为参数)- 它会遍历它们并查找所有唯一元素。由于遍历dict会产生其键,因此它们可以直接传递给union方法。
如果所有输入的键都知道相同,则可以简化此过程:可以硬编码keys(或从其中一个输入中推断出keys),并且列表理解中的if检查变得不必要:
def merge(dicts):
    return {
        k: [d[k] for d in dicts]
        for k in dicts[0].keys()
    }

这类似于blubb的答案,但使用字典理解而不是显式循环来构建最终结果。

我们也可以尝试像Mahdi Ghelichi的答案一样:

def merge(dicts):
    values = zip(*(d.values() for d in ds))
    return dict(zip(dicts[0].keys(), values))

这段代码与Python 3.5及以下版本兼容:具有相同键的字典将在程序运行期间以相同的顺序存储它们(如果再次运行程序,则可能会得到不同的排序,但仍然是一致的)。
在3.6及以上版本中,字典保留其插入顺序(尽管只有在3.7及以上版本中才能保证)。因此,输入的字典可以按不同的顺序具有相同的键,这将导致第一个zip组合错误的值。
我们可以通过“排序”输入字典(重新创建具有一致顺序键的字典,如[ {k:d[k] for k in dicts[0].keys()} for d in dicts])来解决这个问题。(在旧版本中,这将是额外的工作,没有净效果。)然而,这增加了复杂性,这种双重zip方法真的没有比使用字典理解式的先前方法提供任何优势。

显式构建结果,动态发现键

与Eli Bendersky的答案类似,但作为一个函数:
from collections import defaultdict

def merge(dicts):
    result = defaultdict(list)
    for d in dicts:
        for key, value in d.items():
            result[key].append(value)
    return result

这将生成一个defaultdict,它是标准库定义的dict子类。仅使用内置字典的等效代码可能如下所示:
def merge(dicts):
    result = {}
    for d in dicts:
        for key, value in d.items():
            result.setdefault(key, []).append(value)
    return result

使用除列表以外的其他容器类型

使用预先计算键的方法可以很好地创建元组;将列表推导式 [d[k] for d in dicts if k in d] 替换为 tuple(d[k] for d in dicts if k in d)。这将传递一个生成器表达式给 tuple 构造函数。(没有“元组推导式”。)

由于元组是不可变的且没有 append 方法,因此显式循环方法应该通过将 .append(value) 替换为 += (value,) 进行修改。但是,如果存在大量键重复,则可能表现不佳,因为它必须每次创建一个新元组。最好是首先生成列表,然后使用类似于 {k: tuple(v) for (k, v) in merged.items()} 的方法将最终结果转换为元组。

类似的修改也可以用于获取集合(虽然有一个使用 {} 的集合推导式)、Numpy 数组等。例如,我们可以使用像这样的容器类型来概括这两种方法:

def merge(dicts, value_type=list):
    # First, figure out which keys are present.
    keys = set().union(*dicts)
    # Build a dict with those keys, using a list comprehension to
    # pull the values from the source dicts.
    return {
        k: value_type(d[k] for d in dicts if k in d)
        for k in keys
    }

并且

from collections import defaultdict

def merge(dicts, value_type=list):
    # We stick with hard-coded `list` for the first part,
    # because even other mutable types will offer different interfaces.
    result = defaultdict(list)
    for d in dicts:
        for key, value in d.items():
            result[key].append(value)
    # This is redundant for the default case, of course.
    return {k:value_type(v) for (k, v) in result}

如果输入值已经是序列

与其将来自源的值包装在新列表中,通常人们想要接受输入,其中所有值都已经是列表,并在输出中连接这些列表(或连接元组或一维Numpy数组,合并集合等)。

这仍然是一个微不足道的修改。对于预先计算的键,使用嵌套列表推导式,以获取平坦结果的有序序列

def merge(dicts):
    keys = set().union(*dicts)
    return {
        k: [v for d in dicts if k in d for v in d[k]]
        # Alternately:
        # k: [v for d in dicts for v in d.get(k, [])]
        for k in keys
    }

如果您想使用sum来连接原始列表理解的结果,那么请不要这样做 - 当存在大量重复键时,性能会很差。内置的sum没有为序列进行优化(并且将明确禁止“对字符串求和”,并尝试使用每次添加内部创建新列表)。

通过使用显式循环方法,用.extend代替.append

from collections import defaultdict

def merge(dicts):
    result = defaultdict(list)
    for d in dicts:
        for key, value in d.items():
            result[key].extend(value)
    return result

列表的extend方法接受任何可迭代对象,因此对于具有元组值的输入,这将起作用 - 当然,它仍然在输出中使用列表;当然,这些可以像以前展示的那样转换回来。

如果每个输入只有一个项目

这个问题的常见版本涉及每个都有单个键值对的输入字典。或者,输入可以是(key, value)元组(或列表)。

上述方法仍然有效,当然。对于元组输入,首先将它们转换为字典,例如[{k:v} for (k, v) in tuples],可以直接使用它们。或者,可以修改显式迭代方法以直接接受元组,就像Victoria Stuart的答案中所示:

from collections import defaultdict

def merge(pairs):
    result = defaultdict(list)
    for key, value in pairs:
        result[key].extend(value)
    return result

代码被简化了,因为当只有一个键值对并且已经直接提供时,没有必要迭代键值对。

然而,对于这些单项情况,通过按键排序,然后使用 itertools.groupby 可能会更好。在这种情况下,与元组一起工作会更容易。代码如下:

from itertools import groupby

def merge(tuples):
    grouped = groupby(tuples, key=lambda t: t[0])
    return {k: [kv[1] for kv in ts] for k, ts in grouped}

在这里,t被用作输入元组中的一个名称。 grouped迭代器将提供一对“键”值k(是被分组的元组中的第一个元素)和一个迭代器ts,该迭代器遍历该组中的元组。 然后我们从ts中提取键值对kv中的值,将其制作成列表,并将其用作结果字典中k键的值。

当然,要以这种方式合并一个项的字典,首先将它们转换为元组。 为了实现这一点,对于一个由单项字典组成的列表,可以使用一个简单的方法:[next(iter(d.items())) for d in dicts]


1
如果你已经安装了pandas,并且所有字典中的键都相同,那么你可以用一行代码来完成:
import pandas as pd
d1 = {key1: x1, key2: y1}
d2 = {key1: x2, key2: y2}
new_dict = pd.DataFrame([d1,d2]).to_dict('list')

0
假设有两个具有完全相同键的字典,以下是最简洁的方法(应使用python3解决方案)。

d1 = {'a': 1, 'b': 2, 'c':3}
d2 = {'a': 5, 'b': 6, 'c':7} 

# get keys from one of the dictionary
ks = [k for k in d1.keys()]

print(ks)
['a', 'b', 'c']

# call values from each dictionary on available keys
d_merged = {k: (d1[k], d2[k]) for k in ks}

print(d_merged)
{'a': (1, 5), 'b': (2, 6), 'c': (3, 7)}

# to merge values as list
d_merged = {k: [d1[k], d2[k]] for k in ks}
print(d_merged)
{'a': [1, 5], 'b': [2, 6], 'c': [3, 7]}

如果有两个字典具有一些共同的键,但也有一些不同的键,则应准备所有键的列表。

d1 = {'a': 1, 'b': 2, 'c':3, 'd': 9}
d2 = {'a': 5, 'b': 6, 'c':7, 'e': 4} 

# get keys from one of the dictionary
d1_ks = [k for k in d1.keys()]
d2_ks = [k for k in d2.keys()]

all_ks = set(d1_ks + d2_ks)

print(all_ks)
['a', 'b', 'c', 'd', 'e']

# call values from each dictionary on available keys
d_merged = {k: [d1.get(k), d2.get(k)] for k in all_ks}

print(d_merged)
{'d': [9, None], 'a': [1, 5], 'b': [2, 6], 'c': [3, 7], 'e': [None, 4]}


0

有一个很棒的库funcy,只需要一行短小的代码就可以满足你的需求。

from funcy import join_with
from pprint import pprint

d1 = {"key1": "x1", "key2": "y1"}
d2 = {"key1": "x2", "key2": "y2"}

list_of_dicts = [d1, d2]

merged_dict = join_with(tuple, list_of_dicts)

pprint(merged_dict)

输出:

{'key1': ('x1', 'x2'), 'key2': ('y1', 'y2')}

更多信息请参见:funcy -> join_with


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接