列表推导式和生成器推导式(带有`yield`)之间的区别

11
列表解析和生成器解析中带有 `yield` 有什么区别?两者都返回生成器对象(分别为 `listcomp` 和 `genexpr`),但在完全求值后,后者会添加似乎相当多余的 `None`。
区别在于列表解析一次性地构建一个列表对象,而生成器解析则是按需逐个生成元素。因此,即使以相同方式编写,生成器解析中的 `yield` 最终也会返回 `None`。
>>> list([(yield from a) for a in zip("abcde", itertools.cycle("12"))])
['a', '1', 'b', '2', 'c', '1', 'd', '2', 'e', '1']

>>> list(((yield from a) for a in zip("abcde", itertools.cycle("12"))))
['a', '1', None, 'b', '2', None, 'c', '1', None, 'd', '2', None, 'e', '1', None]

怎么会这样?有什么科学解释吗?


1
@Alik,@Antti Haapala,请删除“重复”标记。此问题询问使用yield from Python语句的行为。链接的“重复”答案询问有关yield Python语句的类似问题。这两个语句是不同的。由于yield from最近才添加到语言中,因此它产生的新意外行为是非常自然的。这应该导致一些问题,虽然它们看起来与关于yield的问题相似,但并不是关于yield的相同问题。 - Dmitry Rubanovich
你还在寻找你的问题的答案吗? - Daniel
第一个情况实际上是丢弃了列表推导的结果。该结果来自于推导中的副作用,由yield from a引起。 - MisterMiyagi
3个回答

2

简而言之:生成器表达式使用隐式的yield,从 yield from 表达式返回 None

这里实际上有两个不同的行为。你的列表推导式实际上被丢弃了...

  • 再次清晰地解释一下

如果将表达式转换为等效的函数,则更容易理解。为了清晰起见,让我们把它写出来:

listcomp = [<expr> for a in b]
def listfunc():
    result = []
    for a in b:
        result.append(<expr>)
    return result

gencomp = (<expr> for a in b)
def genfunc():
    for a in b:
        yield <expr>

为了复制初始表达式,关键是用(yield from a)替换<expr>。这是一个简单的文本替换:
def listfunc():
    result = []
    for a in b:
        result.append((yield from a))
    return result

def genfunc():
    for a in b:
        yield (yield from a)

使用 b = ((1,), (2,)),我们期望输出结果为 1, 2。实际上,两者都重复了它们各自的表达式/推导形式的输出。

正如在其他地方所解释的那样,yield (yield from a)应该让你心生疑虑。然而,result.append((yield from a))应该让你感到不安...

  • 产出答案

让我们先看一下生成器。另一种重写方式可以明显地展示正在发生的事情:

def genfunc():
    for a in b:
        result = (yield from a)
        yield result

为了使这个有效,`result`必须有一个值——即`None`。生成器不会 `yield` `(yield from a)`表达式,而是它的结果。只有在评估表达式时才通过副作用获取`a`的内容。
  • 回到问题
如果你检查你的“列表推导”的类型,它不是`list`——是`generator`。 ``只是它的名称。是的,那不是月球,那是一个完全功能的生成器。
记得我们的转换在函数内部放置了一个`yield from` 吗?这就是你定义一个生成器的方式!这是我们的函数版本,这次加了`print`:
def listfunc():
    result = []
    for a in b:
        result.append((yield from a))
        print(result[-1])
    print(result)
    return result

评估list(listfunc()) 会打印出 NoneNone(来自于append),以及[None, None](来自于result)并返回 1, 2。你的实际列表中也包含了那些潜入生成器中的None!但是,它被丢弃了,结果仍然只是一个副作用。这是实际发生的事情:
  • 在评估列表推导式/listfunc时创建一个生成器。
  • 将其提供给list进行迭代...
    • yield from aa的值传递给list,并返回None给推导式/listfunc
    • None存储在结果列表中
  • 在迭代结束时...

    • return引发一个值为[None, None]StopIteration
    • list构造函数忽略此值并将其丢弃
  • 故事的寓意

不要在推导式中使用yield from。它不会做你想象中的那样。


2
在Python 3.7中,推导式和生成器表达式中的 yield 将被弃用,在Python 3.8中则被禁止,因为它会带来很多混淆,并违反了诸如“列表推导式评估为列表”之类的不变量。 - user2357112

1
yield from表达式的值是None。你第二个例子是一个生成器表达式,这意味着它已经在隐式地从迭代器中产出了,因此它也会产生yield from表达式的值。更详细的答案请见this

它并没有解释在生成器推导和列表推导中yield from的求值差异。 - Mischa Arefiev
1
列表推导式没有隐式的yield,因此不会发生这种行为。 - K. Menyah

0

这两个示例在Python 3.8中已被弃用,因为它们容易引起混淆并抛出SyntaxError: 'yield' inside list comprehension错误。请参阅3.8版本发布说明的buglog


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