使用列表推导式从元组列表构建嵌套字典

6

我有一些数据(计数),这些数据通过数据库按照user_idanalysis_type_id进行了索引。这是一个包含3个元素的列表。以下是示例数据:

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]

每个元组的第一项是 count,第二项是 analysis_type_id,最后一项是 user_id

我想把它放到一个字典中,这样我就可以快速检索计数:给定 user_idanalysis_type_id。它必须是一个两级字典。有更好的结构吗?

为了手动构建这个两级字典,我会编写以下代码:

dict = {4:{1:4,5:3,10:2},5:{10:2}}

在这里,user_id 是字典的第一个键级别,analysis_type_id 是第二个(子)键,而 count 则是字典中的值。

我该如何通过列表推导式创建字典中的“双层深度”键?还是说我需要使用嵌套的 for 循环,首先迭代唯一的 user_id 值,然后找到匹配的 analysis_type_id 并逐一填入计数到字典中?


1
你不需要采用双层深度。直接使用两个元素的元组作为字典的键即可。例如:mydict[(4,1)] = 4 - Rick
5个回答

6

Two Tuple Keys

我建议放弃嵌套字典的想法,直接使用两个元组作为键。就像这样:

d = { (user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}

字典是一个哈希表。在Python中,每个二元组有一个单一的哈希值(而不是两个哈希值),因此每个二元组都是基于其(相对)唯一的哈希进行查找的。因此,这比查找两个单独键的哈希值(首先是user_id,然后是analysis_type_id)要快(大多数情况下快2倍)。
但是,注意过早优化。除非您正在执行数百万次查找,否则扁平dict的性能提高不太可能会有影响。支持使用两个值的语法和可读性更好的原因是,假设大多数时间您将根据一对值访问项目,而不是基于单个值的项目组。
考虑使用namedtuple 创建命名元组以存储这些键可能很方便。可以这样做:
from collections import namedtuple
IdPair = namedtuple("IdPair", "user_id, analysis_type_id")

然后在您的字典推导式中使用它:
d = { IdPair(user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}

您可以像这样访问您感兴趣的计数:

somepair = IdPair(user_id = 4, analysis_type_id = 1)
d[somepair]

这种情况有时很有用,因为你可以做这样的事情:
user_id = somepair.user_id # very nice syntax

其他有用的选项

上述解决方案的一个缺点是查找失败的情况。在这种情况下,您只会得到以下类似的回溯信息:

>>> d[IdPair(0,0)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: IdPair(user_id=0, analysis_type_id=0)

这并不是很有帮助。是user_id没有匹配,还是analysis_type_id没有匹配,还是两者都没有匹配?
您可以通过创建自己的dict类型来为自己创建更好的工具,以便获得更多信息和漂亮的回溯。它可能看起来像这样:
class CountsDict(dict):
    """A dict for storing IdPair keys and count values as integers.

    Provides more detailed traceback information than a regular dict.
    """
    def __getitem__(self, k):
        try:
            return super().__getitem__(k)
        except KeyError as exc:
            raise self._handle_bad_key(k, exc) from exc
    def _handle_bad_key(self, k, exc):
        """Provides a custom exception when a bad key is given."""
        try:
            user_id, analysis_type_id = k
        except:
            return exc
        has_u_id = next((True for u_id, _ in self if u_id==user_id), False)
        has_at_id  = next((True for _, at_id in self if at_id==analysis_type_id), False)
        exc_lookup = {(False, False):KeyError(f"CountsDict missing pair: {k}"),
                      (True, False):KeyError(f"CountsDict missing analysis_type_id: "
                                             f"{analysis_type_id}"),
                      (False, True):KeyError(f"CountsDict missing user_id: {user_id}")}
        return exc_lookup[(user_id, analysis_type_id)]

像普通的 dict 一样使用它。

然而,当你尝试访问缺失的键值对时,简单地向你的 dict 添加新的键值对(计数为零)可能更有意义。如果是这种情况,我会使用一个 defaultdict,并在访问缺失的键时将计数设置为零(使用 int 的默认值作为工厂函数)。就像这样:

from collections import defaultdict
my_dict = defaultdict(default_factory=int, 
                      ((user_id, analysis_type_id), count) for count, analysis_type_id, user_id in counts))

现在,如果您尝试访问缺失的键,则计数将被设置为零。但是,这种方法的一个问题是,所有键都将被设置为零:
value = my_dict['I'm not a two tuple, sucka!!!!'] # <-- will be added to my_dict

为了避免这种情况,我们回到制作一个 CountsDict 的想法,但在这种情况下,您的特殊 dict 将是 defaultdict 的子类。然而,与常规的 defaultdict 不同的是,在添加之前它将检查键是否是有效的类型。作为奖励,我们可以确保任何成对元组添加为键时都变成了一个 IdPair
from collections import defaultdict

class CountsDict(defaultdict):
    """A dict for storing IdPair keys and count values as integers.

    Missing two-tuple keys are converted to an IdPair. Invalid keys raise a KeyError.
    """
    def __getitem__(self, k):
        try:
            user_id, analysis_type_id = k
        except:
            raise KeyError(f"The provided key {k!r} is not a valid key.")
        else:
            # convert two tuple to an IdPair if it was not already
            k = IdPair(user_id, analysis_type_id)
        return super().__getitem__(k)

使用它就像普通的defaultdict一样:

my_dict = CountsDict(default_factory=int, 
                     ((user_id, analysis_type_id), count) for count, analysis_type_id, user_id in counts))

注意:上述代码中,我没有将两个元组键转换为IdPair,因为在实例创建时不使用__setitem__。要创建此功能,我们还需要实现__init__方法的覆盖。

总结

在所有这些选项中,更有用的选项完全取决于您的用例。


不错!简单多了。你知道在我调用数据时它是如何运作的吗?比如,如果我编写 d[(4,1)],Python会搜索每个 (user_id,analysis_type_id) 组合直到找到匹配项吗? 我考虑使用双重字典来提高效率:只需查找少量 user_id 以加载相关子字典,然后您只需要搜索少量 analysis_type_id 即可获取正确的计数。 - Ant
字典查找是通过哈希表完成的。这意味着Python基本上直接转到正确的条目,无论有多少条目(O(1)搜索时间)。通过元组进行查找将尽可能快,大约比单独查找元组中的每个条目快两倍。 - Matthias Fripp
那么,对于我的问题而言,使用以下两种类型的答案: 1)键深度为2的defaultdict 2)由2元组组成的“平面”字典 最终预计具有相同的性能,是吗? - Ant
它们的性能不会相同。平面“dict”大多数情况下将快大约2倍,而且很少会变慢。但是,除非您进行数百万次查找,否则这不太重要。使用两个元组解决方案的语法和可读性要好得多,特别是如果您使用“namedtuple”(这是Python中应该自由地使用的绝妙工具)。 - Rick

2
最易读的解决方案是使用一个 defaultdict,它可以避免嵌套循环以及繁琐的检查键是否已存在:
from collections import defaultdict
dct = defaultdict(dict)  # do not shadow the built-in 'dict'
for x, y, z in counts:
    dct[z][y] = x
dct
# defaultdict(dict, {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}})

如果您真的想要一行代码来实现此操作,可以使用itertools.groupby和以下笨拙的方法:
from itertools import groupby
dct = {k: {y: x for x, y, _ in g} for k, g in groupby(sorted(counts, key=lambda c: c[2]), key=lambda c: c[2])}

如果您的初始数据已经按用户ID排序,那么您可以省略排序步骤。

0

您可以使用以下逻辑。无需导入任何包,只需正确使用for循环即可。

counts = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)] dct = {x[2]:{y[1]:y[0] for y in counts if x[2] == y[2]} for x in counts }

"""输出将为 {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}} """


0

使用 defaultdict 对象是个很好的选择。你可以创建一个默认元素为字典的 defaultdict 对象。然后,你就可以把计数器填到正确的字典里面,如下所示:

from collections import defaultdict

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]
dct = defaultdict(dict)
for count, analysis_type_id, user_id in counts:
    dct[user_id][analysis_type_id]=count

dct
# defaultdict(dict, {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}})

# if you want a 'normal' dict, you can finish with this:
dct = dict(dct)

或者你可以使用带有setdefault的标准字典:

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]
dct = dict()
for count, analysis_type_id, user_id in counts:
    dct.setdefault(user_id, dict())
    dct[user_id][analysis_type_id]=count

dct
# {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}}

我认为你无法通过列表推导式来整洁地完成这个任务,但是针对这种情况使用for循环并不需要感到害怕。


0

您可以使用列表推导式来嵌套循环并带有条件,然后使用其中一个或多个来选择元素:

# create dict with tuples
line_dict = {str(nest_list[0]) : nest_list[1:] for nest_list in nest_lists for elem in nest_list if elem== nest_list[0]}
print(line_dict)

 # create dict with list 
line_dict1 = {str(nest_list[0]) list(nest_list[1:]) for nest_list in nest_lists for elem in nest_list if elem== nest_list[0]}
print(line_dict1)

Example: nest_lists = [("a","aa","aaa","aaaa"), ("b","bb","bbb","bbbb") ("c","cc","ccc","cccc"), ("d","dd","ddd","dddd")]

Output: {'a': ('aa', 'aaa', 'aaaa'), 'b': ('bb', 'bbb', 'bbbb'), 'c': ('cc', 'ccc', 'cccc'), 'd': ('dd', 'ddd', 'dddd')}, {'a': ['aa', 'aaa', 'aaaa'], 'b': ['bb', 'bbb', 'bbbb'], 'c': ['cc', 'ccc', 'cccc'], 'd': ['dd', 'ddd', 'dddd']}

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