【问题标题】:Generator expressions Python生成器表达式 Python
【发布时间】:2017-12-08 18:14:47
【问题描述】:

我有一个字典列表,如下所示:

lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]

我写了一个像这样的生成器表达式:

next((itm for itm in lst if itm['a']==5))

现在奇怪的是,虽然这适用于 'a' 的键值对 下次它会为所有其他表达式引发错误。 表达式:

next((itm for itm in lst if itm['b']==6))

错误:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <genexpr>
KeyError: 'b'

【问题讨论】:

    标签: python dictionary generator generator-expression


    【解决方案1】:

    确实,您的结构是一个字典列表

    >>> lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
    

    为了更好地了解您的第一种情况发生了什么,试试这个:

    >>> gen = (itm for itm in lst if itm['a'] == 5)
    >>> next(gen)
    {'a': 5}
    >>> next(gen)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in <genexpr>
    KeyError: 'a'
    

    每次调用next,都会处理下一个元素并返回一个项目。还有……

    next((itm for itm in lst if itm['a'] == 5))
    

    创建一个未分配给任何变量的生成器,处理lst 中的第一个元素,看到键'a' 确实存在,并返回该项目。然后生成器被垃圾收集。没有抛出错误的原因是lst 中的第一项确实包含此密钥。

    因此,如果您将密钥更改为第一项不包含的内容,则会收到您看到的错误:

    >>> gen = (itm for itm in lst if itm['b'] == 6)
    >>> next(gen)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in <genexpr>
    KeyError: 'b'
    

    解决方案

    嗯,已经讨论过的一种解决方案是使用dict.get 函数。这是使用defaultdict 的另一种选择:

    from collections import defaultdict
    from functools import partial
    
    f = partial(defaultdict, lambda: None)
    
    lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
    lst = [f(itm) for itm in lst] # create a list of default dicts
    
    for i in (itm for itm in lst if itm['b'] == 6):
        print(i)
    

    打印出来:

    defaultdict(<function <lambda> at 0x10231ebf8>, {'b': 6})
    

    如果密钥不存在,defaultdict 将返回 None

    【讨论】:

      【解决方案2】:

      这并不奇怪。对于lst 中的每个itm。它将首先评估 filter 子句。现在如果过滤子句是itm['b'] == 6,它将尝试从该字典中获取'b' 键。但是由于first字典没有这样的key,所以会报错。

      对于第一个过滤器示例,这不是问题,因为第一个字典具有一个'a' 键。 next(..) 只对生成器发出的 first 元素感兴趣。所以它从不要求过滤更多元素。

      您可以在此处使用.get(..) 使查找更加安全:

      next((itm for itm in lst if itm<b>.get('b',None)</b>==6))

      如果字典没有这样的键,.get(..) 部分将返回None。由于None 不等于 6,因此过滤器将忽略第一个字典并进一步寻找另一个匹配项。注意,如果不指定默认值None就是默认值,所以等效语句为:

      next((itm for itm in lst if itm.get('b')==6))

      我们也可以省略生成器的括号:只有当有多个参数时,我们才需要这些额外的括号:

      next(itm for itm in lst if itm.get('b')==6)

      【讨论】:

      • 也许只是itm.get('b') == 6None 是默认值)
      • @Chris_Rands:是的,但目的是让None 在这里明确。否则人们想知道None 的来源。但我会将它添加到答案中:)。
      • @WillemVanOnsem 感谢您提供的描述性答案。不过我还有一个问题。由于表达式中有一个 for 循环,我期待如果发生不匹配,表达式将采用列表中的下一个元素。为什么 "d[x]" 不会发生这种情况,而 d.get("x") 会发生这种情况
      • @ApurvaKunkulol:因为第一个导致错误。如果代码引发错误,则执行流程中止,调用堆栈展开,直到有处理错误的捕获机制。在d.get('x') 的情况下,没有这样的错误。因为如果缺少密钥,则返回 None。因此,这将使正常代码路径继续获取下一个itm 并检查该itm 上的过滤器。
      【解决方案3】:

      也许你可以试试这个:

      next(next((itm for val in itm.values() if val == 6) for itm in lst))
      

      这可能有点棘手,它会生成两层generator,因此您需要两个next 才能得到结果。

      【讨论】:

        【解决方案4】:

        分别看一下你的生成器表达式:

        (itm for itm in lst if itm['a']==5)
        

        这将收集列表中itm['a'] == 5 所在的所有项目。到目前为止一切顺利。

        当您在其上调用next() 时,您告诉Python 从该生成器表达式生成first 项。但只有第一个。

        所以当您有条件itm['a'] == 5 时,生成器将获取列表的第一个元素{'a': 5} 并对其执行检查。条件为真,因此该项目由生成器表达式生成并由next() 返回。

        现在,当您将条件更改为itm['b'] == 6 时,生成器将再次获取列表的第一个元素{'a': 5},并尝试获取键为b 的元素。这将失败:

        >>> itm = {'a': 5}
        >>> itm['b']
        Traceback (most recent call last):
          File "<pyshell#1>", line 1, in <module>
            itm['b']
        KeyError: 'b'
        

        它甚至没有机会查看第二个元素,因为它在尝试查看第一个元素时已经失败。

        要解决这个问题,您必须避免在此处使用可以引发KeyError 的表达式。您可以使用dict.get() 尝试检索值而不引发异常:

        >>> lst = [{'a': 5}, {'b': 6}, {'c': 7}, {'d': 8}]
        >>> next((itm for itm in lst if itm.get('b') == 6))
        {'b': 6}
        

        【讨论】:

          【解决方案5】:

          如果字典中没有 'b' 键,itm['b'] 显然会引发 KeyError。一种方法是做

          next((itm for itm in lst if 'b' in itm and itm['b']==6))
          

          如果您不希望在任何字典中出现None,那么您可以将其简化为

          next((itm for itm in lst if itm.get('b')==6))
          

          (由于您与6 比较,这将起作用,但如果您与None 比较,它会给出错误的结果)

          或安全地使用占位符

          PLACEHOLDER = object()
          next((itm for itm in lst if itm.get('b', PLACEHOLDER)==6))
          

          【讨论】:

            猜你喜欢
            • 2014-04-02
            • 2021-12-27
            • 2021-10-17
            • 2021-04-26
            • 2012-08-20
            • 2011-01-01
            • 1970-01-01
            • 2021-11-24
            相关资源
            最近更新 更多