生成器表达式和生成器函数的区别

7
有没有任何区别-性能或其他方面-between生成器表达式和生成器函数?
In [1]: def f():
   ...:     yield from range(4)
   ...:

In [2]: def g():
   ...:     return (i for i in range(4))
   ...:

In [3]: f()
Out[3]: <generator object f at 0x109902550>

In [4]: list(f())
Out[4]: [0, 1, 2, 3]

In [5]: list(g())
Out[5]: [0, 1, 2, 3]

In [6]: g()
Out[6]: <generator object <genexpr> at 0x1099056e0>

我问这个问题是因为我想决定在两者之间如何做出选择。有时生成器函数更清晰,那么选择就很明显。我问的是在代码清晰性不明显的情况下应该如何选择。

2个回答

4
你提供的函数在一般情况下语义完全不同。
第一个函数使用“yield from”将控制权传递给可迭代对象。这意味着在迭代期间调用“send()”和“throw()”将由可迭代对象处理,而不是由你定义的函数处理。
第二个函数仅迭代可迭代对象的元素,并处理所有对“send()”和“throw()”的调用。为了看到区别,请查看以下代码:
In [8]: def action():
   ...:     try:
   ...:         for el in range(4):
   ...:             yield el
   ...:     except ValueError:
   ...:         yield -1
   ...:         

In [9]: def f():
   ...:     yield from action()
   ...:     

In [10]: def g():
    ...:     return (el for el in action())
    ...: 

In [11]: x = f()

In [12]: next(x)
Out[12]: 0

In [13]: x.throw(ValueError())
Out[13]: -1

In [14]: next(x)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-14-5e4e57af3a97> in <module>()
----> 1 next(x)

StopIteration: 

In [15]: x = g()

In [16]: next(x)
Out[16]: 0

In [17]: x.throw(ValueError())
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-17-1006c792356f> in <module>()
----> 1 x.throw(ValueError())

<ipython-input-10-f156e9011f2f> in <genexpr>(.0)
      1 def g():
----> 2     return (el for el in action())
      3 

ValueError: 

事实上,由于这个原因,“yield from”可能比genexp的开销更高,尽管这可能是不相关的。
只有当您想要上述行为或者您正在迭代一个简单的可迭代对象而不是生成器(以便“yield from”等效于循环+简单的“yield”)时,才使用“yield from”。
从风格上讲,我更喜欢:
def h():
    for el in range(4):
        yield el

在处理生成器时,不要使用genexp或使用yield from来返回结果。

实际上,生成器用于执行迭代的代码与上述函数几乎相同:

In [22]: dis.dis((i for i in range(4)).gi_code)
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

正如您所看到的,它执行了 FOR_ITER + YIELD_VALUE。请注意参数 (.0) 是 iter(range(4))。函数的字节码还包含对 LOAD_GLOBALGET_ITER 的调用,这些调用是必需的以查找 range 并获取其可迭代对象。然而,这些操作也必须由生成器表达式执行,只不过不在其代码内部,而是在调用之前执行。


我顺便添加了另一个答案。 - Neil G

1
除了@Bakuriu提到的好处——生成器函数实现send()throw()close()之外,我还遇到了另一个不同之处。有时候,在达到yield语句之前会有一些设置代码。如果该设置代码可能引发异常,则返回生成器版本可能比生成器函数更可取,因为它会更早地引发异常。例如,
def f(x):
    if x < 0:
        raise ValueError
    for i in range(4):
        yield i * i

def g(x):
    if x < 0:
        raise ValueError
    return (i * i for i in range(x))

print(list(f(4)))
print(list(g(4)))
f(-1)  # no exception until the iterator is consumed!
g(-1)

如果想要同时具备这两种行为,我认为以下方法最佳:
def f(count):
    x = 0
    for i in range(count):
        x = yield i + (x or 0)

def protected_f(count):
    if count < 0:
        raise ValueError
    return f(count)

it = protected_f(10)
try:
    print(next(it))
    x = 0
    while True:
        x = it.send(x)
        print(x)
except StopIteration:
    pass

it = protected_f(-1)

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