解析、迭代和生成系列文章:http://www.javashuo.com/article/p-aspbesnv-du.htmlhtml
生成器的wiki页:https://en.wikipedia.org/wiki/Generator_(computer_programming)python
在计算机科学中,生成器是特定的迭代器,它彻底实现了迭代器接口,因此全部生成器都是迭代器。不过,迭代器用于从数据集中取出元素;而生成器用于"凭空"生成(yield)元素。它不会一次性将全部元素所有生成,而是按需一个一个地生成,因此从头至尾都只需占用一个元素的内存空间。git
很典型的一个例子是斐波纳契数列:斐波纳契数列中的数有无穷个,在一个数据结构里放不下,可是能够在须要下一个元素的时候临时计算。数据结构
再好比内置函数range()也返回一个相似生成器的对象,每次须要range里的一个数据时才会临时去产生它。若是必定要让range()函数返回列表,必须明确指明list(range(100))
。并发
在Python中生成器是一个函数,但它的行为像是一个迭代器。另外,Python也支持生成器表达式。app
下面是一个很是简单的生成器示例:函数
>>> def my_generator(chars): ... for i in chars: ... yield i * 2 >>> for i in my_generator("abcdef"): ... print(i, end=" ") aa bb cc dd ee ff
这里的my_generator
是生成器函数(使用了yield关键字的函数,将被声明为generator对象),可是它在for循环中充当的是一个可迭代对象。实际上它自己就是一个可迭代对象:优化
>>> E = my_generator("abcde") >>> hasattr(E, "__iter__") True >>> hasattr(E, "__next__") True >>> E is iter(E) True
因为生成器自动实现了__iter__
和__next__
,且__iter__
返回的是迭代器自身,因此生成器是一个单迭代器,不支持多迭代。编码
此外,生成器函数中使用for来迭代chars变量,但对于chars中被迭代的元素没有其它操做,而是使用yield来返回这个元素,就像return语句同样。code
只不过yield和return是有区别的,yield在生成一个元素后,会记住迭代的位置并将当前的状态挂起(还记住了其它一些必要的东西),等到下一次须要元素的时候再从这里继续yield一个元素,直到全部的元素都被yield完(也可能永远yield不完)。return则是直接退出函数,
当yield的来源为一个for循环,那么能够改写成yield from。也就是说,for i in g:yield i
等价于yield from g
。
例以下面是等价的。
def mygen(chars): yield from chars def mygen(chars): for i in chars: yiled i
yield from更多地用于子生成器的委托,本文暂不对此展开描述。
下面是直接构造出列表的方式,它和前面示例的生成器结果同样,可是内部工做方式是不同的。
def mydef(chars): res = [] for i in chars: res.append(i * 2) return res for i in mydef("abcde"): print(i,end=" ")
这样的结果也能使用列表解析或者map来实现,例如:
for x in [s * 2 for s in "abcde"]: print(x, end=" ") for x in map( (lambda s: s * 2), "abcde" ): print(x, end=" ")
虽然结果上都相同,可是内存使用上和效率上都有区别。直接构造结果集将会等待全部结果都计算完成后一次性返回,可能会占用大量内存并出现结果集等待的现象。而使用生成器的方式,从头至尾都只占用一个元素的内存空间,且无需等待全部元素都计算完成后再返回,因此将时间资源分布到了每一个结果的返回上。
例如总共可能会产生10亿个元素,但只想取前10个元素,若是直接构造结果集将占用巨量内存且等待很长时间,但使用生成器的方式,这10个元素根本不需等待,很快就计算出来。
理解这个工做过程很是重要,是理解和掌握yield的关键。
1.调用生成器函数的时候并无运行函数体中的代码,它仅仅只是返回一个生成器对象。
正以下面的示例,并不是输出任何内容,说明没有执行生成器函数体。
def my_generator(chars): print("before") for i in chars: yield i print("after") >>> c = my_generator("abcd") >>> c <generator object my_generator at 0x000001DC167392A0> >>> I = iter(c)
2.只有开始迭代的时候,才真正开始执行函数体。且在yield以前的代码体只执行一次,在yield以后的代码体只在当前yield结束的时候才执行。
>>> next(I) before # 第一次迭代 'a' >>> next(I) 'b' >>> next(I) 'c' >>> next(I) 'd' >>> next(I) after # 最后一次迭代,抛出异常中止迭代 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
一个生成器函数能够有多个yield语句,看看下面的执行过程:
def mygen(): print("1st") yield 1 print("2nd") yield 2 print("3rd") yield 3 print("end") >>> m = mygen() >>> next(m) 1st 1 >>> next(m) 2nd 2 >>> next(m) 3rd 3 >>> next(m) end Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
到此,想必已经理解了yield的工做过程。但还有一些细节有必要解释清楚。
yield是一个表达式,但它是有返回值的。须要注意的是,yield操做会在产生并发送了值以后当即让函数处于挂起状态,挂起的时候连返回值都还没来得及返回。因此,yield表达式的返回值是在下一次迭代时才生成返回值的。关于yield的返回值相关,见下面的生成器的send()方法。
上面说了,yield有返回值,且其返回值是在下一次迭代的时候才返回的。它的返回值根据恢复yield的方式不一样而不一样。
yield有如下几种常见的表达式组合方式:
yield 10 # (1) 丢弃yield的返回值 x = yield 10 # (2) 将yield返回值赋值给x x = (yield 10) # (3) 等价于 (2) x = (yield 10) + 11 # (4) 将yield返回值加上11后赋值给x
无论yield表达式的编码方式如何,它的返回值都和调用next()(或__next__()
)仍是生成器对象的send()方法有关。这里的send()方法和next()都用于恢复当前挂起的yield。
若是是调用next()来恢复yield,那么yield的返回值为None,若是调用gen.send(XXX)
来恢复yield,那么yield的返回值为XXX
。其实next()能够看做是等价于gen.send(None)
。
再次提醒,yield表达式会在产生一个值后当即挂起,它连返回值都是在下一次才返回的,更不用说yield的赋值和yield的加法操做。
因此,上面的4种yield表达式方式中,若是使用next()来恢复yield,则它们的值分别为:
yield 10 # 先产生10发送出去,而后返回None,但丢弃 x = yield 10 # 返回None,赋值给x x = (yield 10) # 与上等价 x = (yield 10)+11 # 返回None,整个过程报错,由于None和int不能相加
若是使用的是send(100),上面的4种yield表达式方式中的值分别为:
yield 10 # 先产生10发送出去,而后返回100,但丢弃 x = yield 10 # 返回100,赋值给x,x=100 x = (yield 10) # 与上等价 x = (yield 10)+11 # 返回100,加上11后赋值给x,x=111
为了解释清楚yield工做时的返回值问题,我将用两个示例详细地解释每一次next()/send()的过程。
这个示例比较简单。
def mygen(): x = yield 111 # (1) print("x:", x) # (2) for i in range(5): # (3) y = yield i # (4) print("y:", y) # (5) M = mygen()
1.首先执行下面的代码
>>> print("first:",next(M)) 111
这一行执行后,首先将yield出来的111传递给调用者,而后当即在(1)处进行挂起,这时yield表达式尚未进入返回值状态,因此x还未进行赋值操做。可是next(M)
已经返回了,因此print正常输出。
不管是next()(或__next__
)仍是send()均可以用来恢复挂起的yield,但第一次进入yield必须使用next()或者使用send(None)来产生一个挂起的yield。假如第一次就使用send(100)
,因为此时尚未挂起的yield,因此没有yield须要返回值,这会报错。
2.再执行下面的代码
>>> print("second:",M.send(10)) x: 10 second: 0
这里的M.send(10)
首先恢复(1)处挂起的yield,并将10做为该yield的返回值,因此x = 10
,而后生成器函数的代码体继续向下执行,到了print("x:",x)
正常输出。
再继续进入到for循环迭代中,又再次遇到了yield,因而yield产生range(5)的第一个数值0传递给调用者而后当即挂起,因而M.send()
等待到了这个yield值,因而输出"second: 0"。但注意,这时候y尚未进行赋值,由于yield尚未进入返回值的过程。
3.再执行下面的代码
>>> print("third:",M.send(11)) y: 11 third: 1
这里的M.send(11)
首先恢复上次挂起的yield并将11做为该挂起yield的返回值,因此y=11,由于yield已经恢复,因此代码体继续详细执行print("y:",y)
,执行以后进入下一轮for迭代,因而再次遇到yield,它生成第二个range的值1并传递给调用者,而后挂起,因而M.send()
接收到数值1并返回,因而输出third: 1
。注意,此时的y仍然是11,由于for的第二轮yield尚未返回。
4.继续执行,但使用next()
>>> print("fourth:",next(M)) y: None fourth: 2
这里的next(M)恢复前面挂起的yield,而且将None做为yield的返回值,因此y赋值为None。而后进入下一轮for循环、遇到yield,next()接收yield出来的值2并返回。
next()能够看做等价于M.send(None)
。
5.依此类推,直到迭代结束抛出异常
>>> print("fifth:",M.send(13)) y: 13 fifth: 3 >>> print("sixth:",M.send(14)) y: 14 sixth: 4 >>> print("seventh:",M.send(15)) # 看此行 y: 15 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
当发送M.send(15)
时,前面挂起的yield恢复并以15做为返回值,因此y=15
。因而继续执行,但此时for迭代已经完成了,因而抛出异常,整个生成器函数终止。
这个示例稍微复杂些,但理解了前面的yield示例,这个示例也很容易理解。注意,下面的代码不要在交互式python环境中执行,而是以py脚本的方式执行。
def gen(): for i in range(5): X = int((yield i) or 0) + 10 + i print("X:",X) G = gen() for a in G: print(a) G.send(77)
执行结果为:
0 X: 87 X: 11 2 X: 89 X: 13 4 X: 91 Traceback (most recent call last): File "g:\pycode\lists.py", line 10, in <module> G.send(77) StopIteration
这里for a in G
用的是next(),在这个for循环里又用了G.send()
,由于send()接收的值在空上下文,因此被丢弃,但它却将生成器向前移动了一步。
更多的细节请自行思考,如不理解可参考上一个示例的分析。
列表解析/字典解析/集合解析是使用中括号、大括号包围for表达式的,而生成器表达式则是使用小括号包围for表达式,它们的for表达式写法彻底同样。
# 列表解析 >>> [ x * 2 for x in range(5) ] [0, 2, 4, 6, 8] # 生成器表达式 >>> ( x * 2 for x in range(5) ) <generator object <genexpr> at 0x0000013F550A92A0>
在结果上,列表解析等价于list()函数内放生成器表达式:
>>> [ x * 2 for x in range(5) ] [0, 2, 4, 6, 8] >>> list( x * 2 for x in range(5) ) [0, 2, 4, 6, 8]
可是工做方式彻底不同。列表解析等待全部元素都计算完成后一次性返回,而生成器表达式则是返回一个生成器对象,而后一个一个地生成并构建成列表。生成器表达式能够看做是列表解析的内存优化操做,但执行速度上可能要稍慢于列表解析。因此生成器表达式和列表解析之间,在结果集很是大的时候能够考虑采用生成器表达式。
通常来讲,若是生成器表达式做为函数的参数,只要该函数没有其它参数均可以省略生成器表达式的括号,若是有其它参数,则须要括号包围避免歧义。例如:
sum( x ** 2 for x in range(4)) sorted( x ** 2 for x in range(4)) sorted((x ** 2 for x in range(4)),reverse=True)
生成器表达式通常用来写较为简单的生成器对象,生成器函数代码可能稍多一点,但能够实现逻辑更为复杂的生成器对象。它们的关系就像列表解析和普通的for循环同样。
例如,将字母重复4次的生成器对象,能够写成下面两种格式:
# 生成器表达式 t1 = ( x * 4 for x in "hello" ) # 生成器函数 def time4(chars): for x in chars: yield x * 4 t2 = time4("abcd")
map()函数的用法:
map(func, *iterables) --> map object
要想模拟map函数,先看看map()对应的for模拟方式:
def mymap(func,*seqs): res = [] for args in zip(*args): res.append( func(*args) ) return res print( mymap(pow, [1,2,3], [2,3,4,5]) )
对此,能够编写出更精简的列表解析方式的map()模拟代码:
def mymap(func, *seqs): return [ func(*args) for args in zip(*seqs) ] print( mymap(pow, [1,2,3], [2,3,4,5]) )
若是要用生成器来模拟这个map函数,能够参考以下代码:
# 生成器函数方式 def mymap(func, *seqs): res = [] for args in zip(*args): yield func(*args) # 或者生成器表达式方式 def mymap(func, *seqs): return ( func(*args) for args in zip(*seqs) )