关于python:list comprehension,用于从元组列表构建嵌套字典

list comprehension to build a nested dictionary from a list of tuples

我有从数据库中获取的由user_idanalysis_type_id索引的数据(计数)。这是一个三元组的列表。样本数据:

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

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

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

为了"手工"构建两级词典,我将编码:

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

其中user_id是第一个dict键级别,analysis_type_id是第二个(sub-)键,count是dict中的值。

如何通过列表理解在dict键中创建"双深度"?或者,我是否需要使用嵌套的for循环,首先迭代唯一的user_id值,然后找到匹配的analysis_type_id值并填写计数…一次一个地听写?


两个元组键

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

1
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

创建一个命名的元组来存储这些键可能比较方便。这样做:

1
2
from collections import namedtuple
IdPair = namedtuple("IdPair","user_id, analysis_type_id")

然后在字典理解中使用它:

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

访问您感兴趣的计数:

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

这有时很有用的原因是您可以这样做:

1
user_id = somepair.user_id # very nice syntax

。其他一些有用的选择

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

1
2
3
4
>>> 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类型为自己创建一个更好的工具,该类型为您提供了更多信息的良好回溯。它可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的默认值作为工厂函数)。就像这样:

1
2
3
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))

现在,如果尝试访问丢失的密钥,则计数将设置为零。但是,此方法的一个问题是所有键都将设置为零:

1
value = my_dict['I'm not a two tuple, sucka!!!!'] # <-- will be added to my_dict

为了防止这种情况发生,我们回到制作CountsDict的想法,除了这种情况,您的特殊dict将是defaultdict的一个子类。但是,与常规的defaultdict不同,它将在添加之前检查以确保密钥是有效的类型。另外,我们可以确保作为键添加的任何两个元组都成为一个IdPair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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一样使用它:

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

注意:在上面的例子中,我没有这样做,在创建实例时,两个元组键被转换为IdPair(因为在创建实例时没有使用__setitem__)。为了创建这个功能,我们还需要实现对__init__方法的重写。

总结

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


最可读的解决方案是使用defaultdict,它可以保存嵌套循环和不稳定的检查键是否已经存在:

1
2
3
4
5
6
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和这种笨拙的东西:

1
2
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排序,则可以保存排序。


您可以列出对具有条件的嵌套循环的理解,并将其中一个或多个条件用于元素选择:

1
2
3
4
5
6
7
8
9
10
11
# 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']}

您可以使用以下逻辑。它不需要导入任何包,只是我们应该正确地使用for循环。

埃多克斯1〔24〕

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


这对于defaultdict对象是一个很好的用途。可以创建一个元素始终为dict的defaultdict。然后你可以把这些数字输入正确的口述,比如:

1
2
3
4
5
6
7
8
9
10
11
12
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中使用标准听写:

1
2
3
4
5
6
7
8
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循环。