list comprehension从元组列表构建嵌套字典

Ant*_*Ant 6 python dictionary list-comprehension

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

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]
Run Code Online (Sandbox Code Playgroud)

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

我想将它放入字典中,所以我可以快速检索计数:给出a user_idanalysis_type_id.它必须是一个两级字典.有没有更好的结构?

要"手动"构建两级字典,我会编码:

dict = {4:{1:4,5:3,10:2},5:{10:2}}
Run Code Online (Sandbox Code Playgroud)

user_id第一个dict键级别在哪里,analysis_type_id是第二个(子)键,count是dict中的值.

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

Ric*_*ica 6

两个元组键

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

d = { (user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}
Run Code Online (Sandbox Code Playgroud)

字典是哈希表.在python中,每两个元组都有一个哈希值(不是两个哈希值),因此每两个元组都会根据其(相对)唯一哈希来查找.因此,这比查找两个单独的键(首先是user_id,然后是analysis_type_id)的哈希更快(大多数时候快2倍).

但是,要注意过早优化.除非您进行数百万次查询,否则单位性能的提升dict不太重要.在这里支持使用这两个元组的真正原因是两元组解决方案的语法和可读性远远优于其他解决方案 - 也就是说,假设绝大多数时候您希望基于一对值,而不是基于单个值的项目组.

考虑使用a namedtuple

创建用于存储这些键的命名元组可能是方便的.这样做:

from collections import namedtuple
IdPair = namedtuple("IdPair", "user_id, analysis_type_id")
Run Code Online (Sandbox Code Playgroud)

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

d = { IdPair(user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}
Run Code Online (Sandbox Code Playgroud)

并访问您感兴趣的计数,如下所示:

somepair = IdPair(user_id = 4, analysis_type_id = 1)
d[somepair]
Run Code Online (Sandbox Code Playgroud)

这有时候有用的原因是你可以这样做:

user_id = somepair.user_id # very nice syntax
Run Code Online (Sandbox Code Playgroud)

其他一些有用的选择

上述解决方案的一个缺点是查找失败的情况.在这种情况下,您将只获得如下所示的回溯:

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

这不是很有帮助; 它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)]
Run Code Online (Sandbox Code Playgroud)

像普通人一样使用它dict.

但是,dict当您尝试访问缺少的对时,简单地将新对添加到您的(计数为零)可能更有意义.如果是这种情况,我会使用a 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))
Run Code Online (Sandbox Code Playgroud)

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

value = my_dict['I'm not a two tuple, sucka!!!!'] # <-- will be added to my_dict
Run Code Online (Sandbox Code Playgroud)

为了防止这种情况,我们回到制作a的想法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)
Run Code Online (Sandbox Code Playgroud)

像常规一样使用它defaultdict:

my_dict = CountsDict(default_factory=int, 
                     ((user_id, analysis_type_id), count) for count, analysis_type_id, user_id in counts))
Run Code Online (Sandbox Code Playgroud)

注意:在上面我没有这样做,以便IdPair在创建实例时将两个元组键转换为s(因为__setitem__在实例创建期间没有使用).要创建此功能,我们还需要实现__init__方法的覆盖.

包起来

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