Python:生成器expression与收益
在Python中,通过生成器expression式与使用yield语句创build生成器对象有什么不同?
使用收益率 :
def Generator(x, y): for i in xrange(x): for j in xrange(y): yield(i, j)
使用生成器expression式
def Generator(x, y): return ((i, j) for i in xrange(x) for j in xrange(y))
这两个函数都返回产生元组的元素,例如(0,0),(0,1)等
任何一方的优势? 思考?
谢谢大家! 这些答案中有很多很好的信息和进一步的参考资料!
两者只有轻微的差别。 你可以使用dis
模块为自己检查这种事情。
编辑:我的第一个版本反编译在交互式提示中在模块范围创build的生成器expression式。 这与OP在内部使用的版本略有不同。 我修改了这个以匹配问题中的实际案例。
如下所示,“yield”生成器(第一种情况)在设置中有三个额外的指令,但是从第一个FOR_ITER
它们在一个方面有所不同:“yield”方法使用LOAD_FAST
代替LOAD_DEREF
循环。 LOAD_DEREF
比LOAD_FAST
慢得多,所以它使“yield”版本比x
的生成器expression式稍微快一些(外层循环),因为y
的值在每次传递时都会稍微快一些。 对于较小的x
值,由于设置代码的额外开销,它会稍微慢一些。
也许值得指出的是,生成器expression式通常会在代码中内联使用,而不是用类似的函数来包装它。 这将消除一些设置开销,并保持生成器expression式稍微快一些,即使LOAD_FAST
给“yield”版本带来了另一个优势。
在任何情况下,性能差异都不足以certificate两者之间的决定是正确的。 可读性远远超过可读性,所以请select最适合当前情况的可读性。
>>> def Generator(x, y): ... for i in xrange(x): ... for j in xrange(y): ... yield(i, j) ... >>> dis.dis(Generator) 2 0 SETUP_LOOP 54 (to 57) 3 LOAD_GLOBAL 0 (xrange) 6 LOAD_FAST 0 (x) 9 CALL_FUNCTION 1 12 GET_ITER >> 13 FOR_ITER 40 (to 56) 16 STORE_FAST 2 (i) 3 19 SETUP_LOOP 31 (to 53) 22 LOAD_GLOBAL 0 (xrange) 25 LOAD_FAST 1 (y) 28 CALL_FUNCTION 1 31 GET_ITER >> 32 FOR_ITER 17 (to 52) 35 STORE_FAST 3 (j) 4 38 LOAD_FAST 2 (i) 41 LOAD_FAST 3 (j) 44 BUILD_TUPLE 2 47 YIELD_VALUE 48 POP_TOP 49 JUMP_ABSOLUTE 32 >> 52 POP_BLOCK >> 53 JUMP_ABSOLUTE 13 >> 56 POP_BLOCK >> 57 LOAD_CONST 0 (None) 60 RETURN_VALUE >>> def Generator_expr(x, y): ... return ((i, j) for i in xrange(x) for j in xrange(y)) ... >>> dis.dis(Generator_expr.func_code.co_consts[1]) 2 0 SETUP_LOOP 47 (to 50) 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 40 (to 49) 9 STORE_FAST 1 (i) 12 SETUP_LOOP 31 (to 46) 15 LOAD_GLOBAL 0 (xrange) 18 LOAD_DEREF 0 (y) 21 CALL_FUNCTION 1 24 GET_ITER >> 25 FOR_ITER 17 (to 45) 28 STORE_FAST 2 (j) 31 LOAD_FAST 1 (i) 34 LOAD_FAST 2 (j) 37 BUILD_TUPLE 2 40 YIELD_VALUE 41 POP_TOP 42 JUMP_ABSOLUTE 25 >> 45 POP_BLOCK >> 46 JUMP_ABSOLUTE 6 >> 49 POP_BLOCK >> 50 LOAD_CONST 0 (None) 53 RETURN_VALUE
在这个例子中,不是真的。 但是yield
可以用于更复杂的构造 – 例如它也可以接受来自调用者的值,并因此修改stream。 阅读PEP 342更多的细节(这是一个值得了解的有趣的技术)。
无论如何,最好的build议是使用任何更清晰的需求 。
PS这是Dave Beazley的一个简单的协程示例:
def grep(pattern): print "Looking for %s" % pattern while True: line = (yield) if pattern in line: print line, # Example use if __name__ == '__main__': g = grep("python") g.next() g.send("Yeah, but no, but yeah, but no") g.send("A series of tubes") g.send("python generators rock!")
你可以适应一个生成器expression式的简单循环没有区别。 然而,产量可以用来创build更复杂的处理的发电机。 这里是生成斐波那契数列的一个简单的例子:
>>> def fibgen(): ... a = b = 1 ... while 1: ... yield a ... a, b = b, a+b >>> list(itertools.takewhile((lambda x: x<100), fibgen())) [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
如果expression式比嵌套循环更复杂,那么使用yield
是很好的。 除此之外,您可以返回一个特殊的第一个或特殊的最后一个值。 考虑:
def Generator(x): for i in xrange(x): yield(i) yield(None)
在使用中,请注意生成器对象与生成器函数的区别。
生成器对象只能使用一次,与生成器函数相反,每次再次调用它时都可以重用,因为它返回一个新的生成器对象。
发生器expression式在实践中通常使用“原始”,而不包含在函数中,并返回一个生成器对象。
例如:
def range_10_gen_func(): x = 0 while x < 10: yield x x = x + 1 print(list(range_10_gen_func())) print(list(range_10_gen_func())) print(list(range_10_gen_func()))
其输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
与稍有不同的用法比较:
range_10_gen = range_10_gen_func() print(list(range_10_gen)) print(list(range_10_gen)) print(list(range_10_gen))
其输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] []
和一个生成器expression式比较:
range_10_gen_expr = (x for x in range(10)) print(list(range_10_gen_expr)) print(list(range_10_gen_expr)) print(list(range_10_gen_expr))
其中还输出:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] []
在思考迭代器时, itertools
模块:
…标准化一套核心快速,有效的内存工具,这些工具本身或其组合是有用的。 它们一起构成了一个“迭代器代数”,可以在纯Python中简洁高效地构build专门的工具。
对于性能,请考虑itertools.product(*iterables[, repeat])
input迭代的笛卡尔积。
等同于生成器expression式中的嵌套for循环。 例如,
product(A, B)
对于product(A, B)
((x,y) for x in A for y in B)
返回与((x,y) for x in A for y in B)
。
>>> import itertools >>> def gen(x,y): ... return itertools.product(xrange(x),xrange(y)) ... >>> [t for t in gen(3,2)] [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)] >>>
是,有一点不同。
对于生成器expression式(x for var in expr)
,在创buildexpression式时调用iter(expr)
。
当使用def
和yield
创build一个生成器时,如下所示:
def my_generator(): for var in expr: yield x g = my_generator()
iter(expr)
还没有被调用。 它只会在g
迭代时被调用(并且可能根本不会被调用)。
以这个迭代器为例:
from __future__ import print_function class CountDown(object): def __init__(self, n): self.n = n def __iter__(self): print("ITER") return self def __next__(self): if self.n == 0: raise StopIteration() self.n -= 1 return self.n next = __next__ # for python2
此代码:
g1 = (i ** 2 for i in CountDown(3)) # immediately prints "ITER" print("Go!") for x in g1: print(x)
而:
def my_generator(): for i in CountDown(3): yield i ** 2 g2 = my_generator() print("Go!") for x in g2: # "ITER" is only printed here print(x)
由于大多数迭代器不会在__iter__
做很多事情,所以很容易忽略这种行为。 一个真实世界的例子是Django的QuerySet
,它在__iter__
中获取数据,而data = (f(x) for x in qs)
可能需要很多时间,而def g(): for x in qs: yield f(x)
其次是data=g()
会立即返回。
有关更多信息和正式定义,请参阅PEP 289 – 生成器expression式 。
在一些尚未被指出的情况下,这一点可能是重要的。 使用yield
可以防止你使用return
来隐式地提高StopIteration(和协程相关的东西) 。
这意味着这个代码是不合格的(喂给解释器会给你一个AttributeError
):
class Tea: """With a cloud of milk, please""" def __init__(self, temperature): self.temperature = temperature def mary_poppins_purse(tea_time=False): """I would like to make one thing clear: I never explain anything.""" if tea_time: return Tea(355) else: for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']: yield item print(mary_poppins_purse(True).temperature)
另一方面,这个代码就像一个魅力:
class Tea: """With a cloud of milk, please""" def __init__(self, temperature): self.temperature = temperature def mary_poppins_purse(tea_time=False): """I would like to make one thing clear: I never explain anything.""" if tea_time: return Tea(355) else: return (item for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']) print(mary_poppins_purse(True).temperature)