参考:http://youchen.me/2017/02/10/Python-What-does-yield-do/#
为了搞清楚yield
是用来做什么的,你首先得知道Python中生成器的相关概念,而为了了解生成器的相关概念,你需要知道什么是迭代器。
迭代器
当你创建一个了列表,你可以逐个遍历列表中的元素,而这个过程便叫做迭代:
>>> mylist = [1, 2, 3] >>> for i in mylist: ... print(i) 1 2 3
而mylist
是一个可迭代对象。当你使用列表推导式的时候,创建了一个列表,他也是可迭代对象:
>>> mylist = [x*x for x in range(3)] >>> for i in mylist: ... print(i) 0 1 4
所有能够接受for...in...
操作的对象都是可迭代对象,如列表、字符串、文件等。这些可迭代对象用起来都十分顺手.
因为你可以按照你的想法去访问它们,但是你把所有数据都保存在了内存中,而当你有大量数据的时候这可能并不是你想要的结果。
生成器
生成器也是迭代器,但是你只能对它们进行一次迭代,原因在于它们并没有将所有数据存储在内存中,而是即时生成这些数据:
>>> mygenerator = (x*x for x in range(3)) >>> for i in mygenerator: ... print(i) 0 1 4
这一段代码和上面那段很相似,唯一不同的地方是使用了()
代替[]
。但是,这样的后果是你无法对mygenerator
进行第二次for
,因为生成器只能被使用一次:它首先计算出结果0,然后忘记它再计算出1,最后是4,一个接一个。
i in mygenerator
Yield
yield
是一个用法跟return
很相似的关键字,不同在于函数返回的是一个生成器。
>>> def createGenerator(): ... mylist = range(3) ... for i in mylist: ... yield i*i ... >>> mygenerator = createGenerator() # create a generator >>> print(mygenerator) # mygenerator is an object! <generator object createGenerator at 0xb7555c34> >>> for i in mygenerator: ... print(i) 0 1 4
这是一个没有什么用的例子,但是用来让你了解当你知道你的函数会返回一个只会被遍历1次的巨大数据集合该怎么做的时候十分方便。为了掌握yield
,你必须了解当你调用这个函数的时候,你在函数体中写的代码并没有被执行,而是只返回了一个生成器对象,这个需要特别注意。然后,你的代码将会在每次for
使用这个生成器的时候被执行。最后,最困难的部分:
for
第一次调用通过你函数创建的生成器对象的时候,它将会从你函数的开头执行代码,一直到到达yield
,然后它将会返回循环中的第一个值。然后,其他每次调用都会再一次执行你在函数中写的那段循环,并返回下一个值,直到没有值可以返回。
生成器在函数执行了却没有到达yield
的时候将被认为是空的,原因在于循环到达了终点,或者不再满足if/else
条件。
>>> class Bank(): # let's create a bank, building ATMs ... crisis = False ... def create_atm(self): ... while not self.crisis: ... yield "$100" >>> hsbc = Bank() # when everything's ok the ATM gives you as much as you want >>> corner_street_atm = hsbc.create_atm() >>> print(corner_street_atm.next()) $100 >>> print(corner_street_atm.next()) $100 >>> print([corner_street_atm.next() for cash in range(5)]) ['$100', '$100', '$100', '$100', '$100'] >>> hsbc.crisis = True # crisis is coming, no more money! >>> print(corner_street_atm.next()) <type 'exceptions.StopIteration'> >>> wall_street_atm = hsbc.create_atm() # it's even true for new ATMs >>> print(wall_street_atm.next()) <type 'exceptions.StopIteration'> >>> hsbc.crisis = False # trouble is, even post-crisis the ATM remains empty >>> print(corner_street_atm.next()) <type 'exceptions.StopIteration'> >>> brand_new_atm = hsbc.create_atm() # build a new one to get back in business >>> for cash in brand_new_atm: ... print cash $100 $100 $100 $100 $100 $100 $100 $100 $100 ...
首先看生成器的next
方法,它用来执行代码并从生成器中获取下一个元素(在Python 3.x中生成器已经没有next方法,而是使用next(iterator)代替)。在crisis
未被置为True
的时候,create_atm
函数中的while
循环可以看做是无尽的,当crisis
为True
的时候,跳出了while
循环,所有迭代器将会到达函数尾部,此时再次访问next
将会抛出StopIteration
异常,而此时就算将crisis
设置为False
,这些生成器仍然处在函数尾部,访问会继续抛出StopIteration
异常。
将以上例子用来控制访问资源等用途的时候十分有用。
itertools
,你的好朋友
itertools
模块包含了许多用来操作可迭代对象的函数。想复制一个生成器?向连接两个生成器?想把多个值组合到一个嵌套列表里面?使用map/zip
而不用重新创建一个列表?那么就:import
吧。
itertools
让我们来看看四匹马赛跑可能的到达结果:
>>> horses = [1, 2, 3, 4] >>> races = itertools.permutations(horses) >>> print(races) <itertools.permutations object at 0xb754f1dc> >>> print(list(itertools.permutations(horses))) [(1, 2, 3, 4), (1, 2, 4, 3), (1, 3, 2, 4), (1, 3, 4, 2), (1, 4, 2, 3), (1, 4, 3, 2), (2, 1, 3, 4), (2, 1, 4, 3), (2, 3, 1, 4), (2, 3, 4, 1), (2, 4, 1, 3), (2, 4, 3, 1), (3, 1, 2, 4), (3, 1, 4, 2), (3, 2, 1, 4), (3, 2, 4, 1), (3, 4, 1, 2), (3, 4, 2, 1), (4, 1, 2, 3), (4, 1, 3, 2), (4, 2, 1, 3), (4, 2, 3, 1), (4, 3, 1, 2), (4, 3, 2, 1)]
迭代的内部机理
迭代是一个依赖于可迭代对象(需要实现__iter__()
方法)和迭代器(需要实现__next__()
方法)的过程。
可迭代对象是任意你可以从中得到一个迭代器的对象。
迭代器是让你可以对可迭代对象进行迭代的对象。
总结
yield
语句将你的函数转化成一个能够生成一种能够包装你原函数体的名叫生成器的特殊对象的工厂。当生成器被迭代,它将会起始位置开始执行函数一直到到达下一个yield
,然后挂起执行,计算返回传递给yield
的值,它将会在每次迭代的时候重复这个过程直到函数执行到达函数的尾部,举例来说:
def simple_generator(): yield 'one' yield 'two' yield 'three' for i in simple_generator(): print i 输出结果为: one two three
这种效果的产生是由于在循环中使用了可以产生序列的生成器,生成器在每次循环时执行代码到下一个yield
,并计算返回结果,这样生成器即时生成了一个列表,这对于特别是大型计算来说内存节省十分有效。
假设你想实现自己的可以产生一个可迭代一定范围数的range
函数(特指Python 2.x中的range
),你可以这样做和使用:
def myRangeNaive(i): n = 0 range = [] while n < i: range.append(n) n = n + 1 return range for i in myRangeNaive(10): print i
但是这样并不高效,原因1:你创建了一个你只会使用一次的列表;原因2:这段代码实际上循环了两次。
由于Guido和他的团队很慷慨地开发了生成器因此我们可以这样做:
def myRangeSmart(i): n = 0 while n < i: yield n n = n + 1 return for i in myRangeSmart(10): print i
现在,每次对生成器迭代将会调用next()
来执行函数体直到到达yield
语句,然后停止执行,并计算返回结果,或者是到达函数体尾部。在这种情况下,第一次的调用next()
将会执行到yield
并返回
nn
,下一次的next()
将会执行自增操作,然后回到while
的判断,如果满足条件,则再一次停止并返回n
,它将会以这种方式执行一直到不满足while
条件,使得生成器到达函数体尾部。
生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。
生成器不仅“记住”了它数据状态;生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。
生成器的特点:
1.生成器是一个函数,而且函数的参数都会保留。
2.迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的
3.节约内存
例子:执行到yield时,gen函数暂时停止并保存,返回x的值,同时tmp接收send的值(ps:yield x 相当于 return x ,所以第一次c.next()结果是0。第二次c.next()时,继续在原来暂停的地方执行,因为没有send 值,所以tmp 为 None。c.next()等价c.send(None))。下次c.send(“python”),send发送过来的值,c.next()等价c.send(None)
了解了next()如何让包含yield的函数执行后,我们再来看另外一个非常重要的函数send(msg)。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做c.next()
和 c.send(None) 作用是一样的。
需要提醒的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错的,因为没有Python yield语句来接收这个值。
#generation.py def gen(): for x in xrange(4): tmp = yield x if tmp == "hello": print "world" else: print "12345abcd", str(tmp) >>>from generation import gen >>>c=gen() >>>c.next() 0 >>>c.next() 12345abcd None 1 >>>c.send("python") 12345abcd python2
来源: https://www.oschina.net/translate/improve-your-python-yield-and-generators-explained
我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None)。一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。
对于在计算机编程中所讨论的函数,这是很标准的流程。这样的函数只能返回一个值,不过,有时可以创建能产生一个序列的函数还是有帮助的。要做到这一点,这种函数需要能够“保存自己的工作”。
我说过,能够“产生一个序列”是因为我们的函数并没有像通常意义那样返回。return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而"yield"的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。
在Python中,拥有这种能力的“函数”被称为生成器,它非常的有用。生成器(以及yield语句)最初的引入是为了让程序员可以更简单的编写用来产生值的序列的代码。 以前,要实现类似随机数生成器的东西,需要实现一个类或者一个模块,在生成数据的同时保持对每次调用之间状态的跟踪。引入生成器之后,这变得非常简单。
为了更好的理解生成器所解决的问题,让我们来看一个例子。在了解这个例子的过程中,请始终记住我们需要解决的问题:生成值的序列。
生成器。一个生成器会“生成”值。创建一个生成器几乎和生成器函数的原理一样简单。
一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。除了以上内容,创建一个生成器没有什么多余步骤了。
生成器函数返回生成器的迭代器。这可能是你最后一次见到“生成器的迭代器”这个术语了, 因为它们通常就被称作“生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是__next__()。如同迭代器一样,我们可以使用next()函数来获取下一个值。
为了从生成器获取下一个值,我们使用next()函数,就像对付迭代器一样。
(next()会操心如何调用生成器的__next__()方法)。既然生成器是一个迭代器,它可以被用在for循环中。
每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。**
yield就是专门给生成器用的return(加上点小魔法)。
下面是一个简单的生成器函数:
>>> def simple_generator_function(): >>> yield 1 >>> yield 2 >>> yield 3
这里有两个简单的方法来使用它:
>>> for value in simple_generator_function(): >>> print(value) 1 2 3 >>> our_generator = simple_generator_function() >>> next(our_generator) 1 >>> next(our_generator) 2 >>> next(our_generator) 3
那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。
我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。
def get_primes(number): while True: if is_prime(number): yield number number += 1
如果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。 这会通知next()的调用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while循环在我们的get_primes()函数出现的原因。如果没有这个while,当我们第二次调用next()的时候,生成器函数会执行到函数末尾,触发StopIteration异常。一旦生成器的值用完了,再调用next()就会出现错误,所以你只能将每个生成器的使用一次。下面的代码是错误的:
>>> our_generator = simple_generator_function() >>> for value in our_generator: >>> print(value) >>> # 我们的生成器没有下一个值了... >>> print(next(our_generator)) Traceback (most recent call last): File "<ipython-input-13-7e48a609051a>", line 1, in <module> next(our_generator) StopIteration >>> # 然而,我们总可以再创建一个生成器 >>> # 只需再次调用生成器函数即可 >>> new_generator = simple_generator_function() >>> print(next(new_generator)) # 工作正常 1
因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。
执行流程
让我们回到调用get_primes的地方:solve_number_10。
def solve_number_10(): # She *is* working on Project Euler #10, I knew it! total = 2 for next_prime in get_primes(3): if next_prime < 2000000: total += next_prime else: print(total) return
我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素是如何创建的有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。
- 进入第三行的while循环
- 停在if条件判断(3是素数)
- 通过yield将3和执行控制权返回给solve_number_10
接下来,回到insolve_number_10:
- for循环得到返回值3
- for循环将其赋给next_prime
- total加上next_prime
- for循环从get_primes请求下一个值
这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。
def get_primes(number): while True: if is_prime(number): yield number number += 1 # <<<<<<<<<<
最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方,同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。
更给力点
在PEP 342中加入了将值传给生成器的支持。PEP
342加入了新的特性,能让生成器在单一语句中实现,生成一个值(像从前一样),接受一个值,或同时生成一个值并接受一个值。
我们用前面那个关于素数的函数来展示如何将一个值传给生成器。这一次,我们不再简单地生成比某个数大的素数,而是找出比某个数的等比级数大的最小素数(例如10, 我们要生成比10,100,1000,10000 ... 大的最小素数)。我们从get_primes开始:
def print_successive_primes(iterations, base=10): # 像普通函数一样,生成器函数可以接受一个参数 prime_generator = get_primes(base) # 这里以后要加上点什么 for power in range(iterations): # 这里以后要加上点什么 def get_primes(number): while True: if is_prime(number): # 这里怎么写?
get_primes的后几行需要着重解释。yield关键字返回number的值,而像 other = yield foo 这样的语句的意思是,"返回foo的值,这个值返回给调用者的同时,将other的值也设置为那个值"。你可以通过send方法来将一个值”发送“给生成器。
def get_primes(number): while True: if is_prime(number): number = yield number number += 1
通过这种方式,我们可以在每次执行yield的时候为number设置不同的值。现在我们可以补齐print_successive_primes中缺少的那部分代码:
def print_successive_primes(iterations, base=10): prime_generator = get_primes(base) prime_generator.send(None) for power in range(iterations): print(prime_generator.send(base ** power))
这里有两点需要注意:首先,我们打印的是generator.send的结果,这是没问题的,因为send在发送数据给生成器的同时还返回生成器通过yield生成的值(就如同生成器中yield语句做的那样)。
第二点,看一下prime_generator.send(None)这一行,当你用send来“启动”一个生成器时(就是从生成器函数的第一行代码执行到第一个yield语句的位置),你必须发送None。这不难理解,根据刚才的描述,生成器还没有走到第一个yield语句,如果我们发生一个真实的值,这时是没有人去“接收”它的。一旦生成器启动了,我们就可以像上面那样发送数据了。
综述
在本系列文章的后半部分,我们将讨论一些yield的高级用法及其效果。yield已经成为Python最强大的关键字之一。现在我们已经对yield是如何工作的有了充分的理解,我们已经有了必要的知识,可以去了解yield的一些更“费解”的应用场景。
不管你信不信,我们其实只是揭开了yield强大能力的一角。例如,send确实如前面说的那样工作,但是在像我们的例子这样,只是生成简单的序列的场景下,send几乎从来不会被用到。下面我贴一段代码,展示send通常的使用方式。对于这段代码如何工作以及为何可以这样工作,在此我并不打算多说,它将作为第二部分很不错的热身。
import random def get_data(): """返回0到9之间的3个随机数""" return random.sample(range(10), 3) def consume(): """显示每次传入的整数列表的动态平均值""" running_sum = 0 data_items_seen = 0 while True: data = yield data_items_seen += len(data) running_sum += sum(data) print('The running average is {}'.format(running_sum / float(data_items_seen))) def produce(consumer): """产生序列集合,传递给消费函数(consumer)""" while True: data = get_data() print('Produced {}'.format(data)) consumer.send(data) yield if __name__ == '__main__': consumer = consume() consumer.send(None) producer = produce(consumer) for _ in range(10): print('Producing...') next(producer)
来源:http://blog.csdn.net/yueguanghaidao/article/details/10201327
yield指令,可以暂停一个函数并返回中间结果。使用该指令的函数将保存执行环境,并且在必要时恢复。
生成器比迭代器更加强大也更加复杂,需要花点功夫好好理解贯通。
看下面一段代码:
[python] view
plain copy
- def gen():
- for x in xrange(4):
- tmp = yield x
- if tmp == 'hello':
- print 'world'
- else:
- print str(tmp)
只要函数中包含yield关键字,该函数调用就是生成器对象。
[python] view
plain copy
- g=gen()
- print g #<generator object gen at 0x02801760>
- print isinstance(g,types.GeneratorType) #True
我们可以看到,gen()并不是函数调用,而是产生生成器对象。
生成器对象支持几个方法,如gen.next() ,gen.send() ,gen.throw()等。
[python] view
plain copy
- print g.next() # 0
调用生成器的next方法,将运行到yield位置,此时暂停执行环境,并返回yield后的值。所以打印出的是0,暂停执行环境。
[python] view
plain copy
- print g.next() #None 1
再调用next方法,你也许会好奇,为啥打印出两个值,不急,且听我慢慢道来。
上一次调用next,执行到yield 0暂停,再次执行恢复环境,给tmp赋值(注意:这里的tmp的值并不是x的值,而是通过send方法接受的值),由于我们没有调用send方法,所以
tmp的值为None,此时输出None,并执行到下一次yield x,所以又输出1.
到了这里,next方法我们都懂了,下面看看send方法。
[python] view
plain copy
- print g.send('hello') #world 2
上一次执行到yield 1后暂停,此时我们send('hello'),那么程序将收到‘hello',并给tmp赋值为’hello',此时tmp=='hello'为真,所以输出'world',并执行到下一次yield 2,所以又打印出2.(next()等价于send(None))
当循环结束,将抛出StopIteration停止生成器。
看下面代码:
[python] view
plain copy
- def stop_immediately(name):
- if name == 'skycrab':
- yield 'okok'
- else:
- print 'nono'
- s=stop_immediately('sky')
- s.next()
正如你所预料的,打印出’nono',由于没有额外的yield,所以将直接抛出StopIteration。
[python] view
plain copy
- nono
- Traceback (most recent call last):
- File "F:\python workspace\Pytest\src\cs.py", line 170, in <module>
- s.next()
- StopIteration
看下面代码,理解throw方法,throw主要是向生成器发送异常。
[python] view
plain copy
- def mygen():
- try:
- yield 'something'
- except ValueError:
- yield 'value error'
- finally:
- print 'clean' #一定会被执行
- gg=mygen()
- print gg.next() #something
- print gg.throw(ValueError) #value error clean
调用gg.next很明显此时输出‘something’,并在yield ‘something’暂停,此时向gg发送ValueError异常,恢复执行环境,except 将会捕捉,并输出信息。
理解了这些,我们就可以向协同程序发起攻击了,所谓协同程序也就是是可以挂起,恢复,有多个进入点。其实说白了,也就是说多个函数可以同时进行,可以相互之间发送消息等。
这里有必要说一下multitask模块(不是标准库中的),看一段multitask使用的简单代码:
[python] view
plain copy
- def tt():
- for x in xrange(4):
- print 'tt'+str(x)
- yield
- def gg():
- for x in xrange(4):
- print 'xx'+str(x)
- yield
- t=multitask.TaskManager()
- t.add(tt())
- t.add(gg())
- t.run()
结果:
[python] view
plain copy
- tt0
- xx0
- tt1
- xx1
- tt2
- xx2
- tt3
- xx3
如果不是使用生成器,那么要实现上面现象,即函数交错输出,那么只能使用线程了,所以生成器给我们提供了更广阔的前景。
如果仅仅是实现上面的效果,其实很简单,我们可以自己写一个。主要思路就是将生成器对象放入队列,执行send(None)后,如果没有抛出StopIteration,将该生成器对象再加入队列。
[python] view
plain copy
- class Task():
- def __init__(self):
- self._queue = Queue.Queue()
- def add(self,gen):
- self._queue.put(gen)
- def run(self):
- while not self._queue.empty():
- for i in xrange(self._queue.qsize()):
- try:
- gen= self._queue.get()
- gen.send(None)
- except StopIteration:
- pass
- else:
- self._queue.put(gen)
- t=Task()
- t.add(tt())
- t.add(gg())
- t.run()
当然,multitask实现的肯定不止这个功能,有兴趣的童鞋可以看下源码,还是比较简单易懂的。
#增补 2014/5/21
之前我在南京面试Python时遇到这么一道题目:
[python] view
plain copy
- def thread1():
- for x in range(4):
- yield x
- def thread2():
- for x in range(4,8):
- yield x
- threads=[]
- threads.append(thread1())
- threads.append(thread2())
- def run(threads): #写这个函数,模拟线程并发
- pass
- run(threads)
如果上面class Task看懂了,那么这题很简单,其实就是考你用yield模拟线程调度,解决如下:
[python] view
plain copy
- def run(threads):
- for t in threads:
- try:
- print t.next()
- except StopIteration:
- pass
- else:
- threads.append(t)