生成器表达式和生成器函数之间是否存在性能差异?
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>
我问,因为我想决定如何决定如何使用这两者。有时发电机功能更清晰,然后选择很明确。我问的是代码清晰度没有明显选择的那些时候。
答案 0 :(得分:3)
在一般情况下,您提供的功能具有完全不同的语义。
第一个,yield from
,将控件传递给iterable。这意味着在迭代期间对send()
和throw()
的调用将由iterable处理,而不是由您定义的函数处理。
第二个函数只迭代迭代的元素,它将处理对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
s)。
从文体上讲,我更喜欢:
yield
在处理生成器时,而不是def h():
for el in range(4):
yield el
一个genexp或使用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
并获取其可迭代所必需的。但是这个动作也必须由genexp执行,不是在代码内部,而是在调用之前。
答案 1 :(得分: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)