生成器表达式和生成器函数的区别
生成器表达式和生成器函数之间有没有什么区别,比如性能方面或者其他方面的区别呢?
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 个回答
除了@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)
你提供的这两个函数在一般情况下的意思是完全不同的。
第一个函数使用了 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 from
就相当于一个循环加上简单的 yield
。
从风格上讲,我更喜欢:
def h():
for el in range(4):
yield el
而不是在处理生成器时使用 return
返回一个生成器表达式或使用 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_GLOBAL
和 GET_ITER
的部分,这些是用来查找 range
并获取它的可迭代对象的。不过,这些操作生成器表达式也必须执行,只是它们不是在生成器表达式的代码内部,而是在调用它之前进行的。