反转值为列表的字典的Pythonic方法是什么?

3

我有一个类似于这样的字典:

letters_by_number = {
  1: ['a', 'b', 'c', 'd'],
  2: ['b', 'd'],
  3: ['a', 'c'],
  4: ['a', 'd'],
  5: ['b', 'c']
}

我希望将其反转,使其看起来像这样:

numbers_by_letter = {
  'a': [1, 3, 4],
  'b': [1, 2, 5],
  'c': [1, 3, 5],
  'd': [1, 2, 4]
}

我知道可以通过循环遍历letters_by_number中的键值对,然后再循环遍历value(即列表),并将(val,key)添加到字典中的列表中来实现此操作。这样做很麻烦,我感觉一定有更多“Pythonic”的方法。有什么建议吗?


这个回答解决了您的问题吗?如何将字典中的列表值翻转 - Georgy
这个回答解决了你的问题吗?如何在Python中反转一个值为列表的字典? - mkrieger1
4个回答

7

这非常适合于collections.defaultdict

>>> from collections import defaultdict
>>> numbers_by_letter = defaultdict(list)
>>> for k, seq in letters_by_number.items():
...     for letter in seq:
...         numbers_by_letter[letter].append(k)
... 
>>> dict(numbers_by_letter)
{'a': [1, 3, 4], 'b': [1, 2, 5], 'c': [1, 3, 5], 'd': [1, 2, 4]}

请注意,您实际上不需要最终的dict()调用(defaultdict已经为您提供了您可能想要的行为),但我在这里包含它是因为你的问题的结果是dict类型。

实际结果是:defaultdict(<type 'list'>, {'a': [1, 3, 4], 'c': [1, 3, 5], 'b': [1, 2, 5], 'd': [1, 2, 4]}),因为 numbers_by_letter 是一个 defaultdict - martineau

1
使用setdefault
letters_by_number = {
    1: ['a', 'b', 'c', 'd'],
    2: ['b', 'd'],
    3: ['a', 'c'],
    4: ['a', 'd'],
    5: ['b', 'c']
}

inv = {}
for k, vs in letters_by_number.items():
    for v in vs:
        inv.setdefault(v, []).append(k)

print(inv)

输出

{'a': [1, 3, 4], 'b': [1, 2, 5], 'c': [1, 3, 5], 'd': [1, 2, 4]}

1
一个(微不足道的)dict子类将使这变得非常容易:
class ListDict(dict):
    def __missing__(self, key):
        value = self[key] = []
        return value


letters_by_number = {
  1: ['a', 'b', 'c', 'd'],
  2: ['b', 'd'],
  3: ['a', 'c'],
  4: ['a', 'd'],
  5: ['b', 'c']
}


numbers_by_letter = ListDict()
for key, values in letters_by_number.items():
    for value in values:
        numbers_by_letter[value].append(key)

from pprint import pprint
pprint(numbers_by_letter, width=40)

输出:

{'a': [1, 3, 4],
 'b': [1, 2, 5],
 'c': [1, 3, 5],
 'd': [1, 2, 4]}

2
看起来这正是collections.defaultdict类所做的事情!你知道得越多! - jxmorris12
1
@jxmorris12:在很大程度上,它实现了相同的功能,但一个区别是它不定义自己的'__repr __()'或'__str__()'方法,因此实例通常在显示时看起来像普通字典。另一个可能的优点是,根据您计划对结果进行的操作,'__missing__()'方法可以被设置为执行其他非平凡的操作。 - martineau
1
@martineau 不过,作为子类,它的__repr__不应该有所不同吗?像这样 ListDict({'a': [1, 3, 4], ...}) - wjandrea
1
@wjandrea:是的,从技术上讲——根据文档——它“应该”如此,因此返回的字符串可以用于重新创建对象。但是,如果您定义了一个,则它也将用作__str __()方法,除非您还定义了该方法。这很快使类变得不太平凡,这有点违背了它的主要目的(效率)。 - martineau

0
这是一种使用字典推导式的解决方案,而不需要在循环中添加列表元素。通过将所有列表合并来构建键的集合,然后使用列表推导式构建每个列表。为了更加高效,我首先构建了另一个包含集合而不是列表的字典,使得 k in v 成为一个 O(1) 操作。
from itertools import chain

def invert_dict_of_lists(d):
    d = { i: set(v) for i, v in d.items() }
    return {
        k: [ i for i, v in d.items() if k in v ]
        for k in set(chain.from_iterable(d.values()))
    }

严格来说,在Python 3的现代版本中,字典保留了键插入的顺序。这会产生一个结果,其中键按照它们在列表中出现的顺序排列;而不是像你的示例中那样按字母顺序排列。如果你确实希望按排序顺序获取键,则将for k in set(...)更改为for k in sorted(set(...))

就我个人而言,我认为使用循环而不是推导式并没有违反Python的风格。Brad的回答更加简洁易懂,而且至少和使用推导式一样高效;他的方法也是我自己解决这个问题的方式。尽管如此,我还是发布了这个答案,因为问题暗示你想在不使用循环的情况下完成它。 - kaya3

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