生成器与yield
函数使用yield关键字可以定义生成器对象。生成器是一个函数。它生成一个值的序列,以便在迭代中使用,例如:
1 def countdown(n): 2 print('倒计时:%s' % n) 3 while n > 0: 4 yield n 5 n -= 1 6 return 7 8 c = countdown(10)
如果调用该函数,就会发现其中的代码不会开始执行,相反它会返回一个生成器对象,接着该生成器对象就会在__next__()被调用时执行函数。
print(c.__next__()) print(c.__next__()) print(c.__next__())
调用__next__()时,生成器函数将开始执行语句,知道遇到yield语句为止。yield语句在函数执行停止的地方生成一个结果,直到再次调用next()。然后继续执行yield()之后的语句。通常不会在生成器上直接调用next()方法,而是通过for语句、sum()或一些消耗序列的其他操作使用生成器。例如:
for i in countdown(10): print(i) a = sum(countdown(10)) print(a)
生成器函数完成的标志是返回或引发StopIteration异常,这标志着迭代的结束。如果生成器在完成时返回None以外的值都是不合法的。生成器使用时存在一个棘手的问题,即生成器函数仅被部分消耗,例如:
for n in countdown(10): if n == 2: break print(n)
在这个例子中,通过调用break退出循环,而相关的生成器也没有全部完成。为了处理这种情况,生成器对象提供方法close()标识关闭。不再使用或删除生成器时,就会调用close()方法。通常不必手动调用close()方法,但也可以这么做。
在生成器函数内部,在yield语句上出现GeneratorExit异常时就会调用close()方法。也可以选择捕捉这个异常,以便执行清理操作
1 def countdown2(n): 2 print('倒计时:%s' % n) 3 try: 4 while n > 0: 5 yield n 6 n -= 1 7 except GeneratorExit: 8 print('GeneratorExit %s' % n) 9 10 c = countdown2(2) 11 12 print(next(c)) 13 print(next(c)) 14 del c
虽然可以捕捉GenratorExit异常,但对于生成器函数而言,使用yield语句处理异常并生成另一个输出值是不合法的。另外,如果程序当前正在对生成器进行迭代,不应该通过另一个的执行线程或从信号处理程序异步调用该生成器上的close()方法。
协程与yield表达式
在函数内, yield语句还可以作为表达式使用,出现在赋值运算符的右边,例如:
def receive(): print('Ready to receive') while True: n = yield print('Got %s' % n)
以这种方式使用yield语句的函数称为协程,向函数发送值时函数将执行。它的行为也十分类似于生成器
r = receive() r.__next__() r.send(1)
在这个例子中,一开始调用__next__()是必不可少的,这样协程才能执行第一个yield表达式之前的语句。这时,协程会挂起,等待相关生成器对象r的send()方法给他发送一个值。
传递给send()的值由协程中的yield表达式返回。接收到值后,协程就会执行语句,直到遇到下一条yield语句。
在协程中需要调用next()这件事很容易被忽略,这经常称为错误出现的原因。因此,建议使用一个能够自动完成该步骤的装饰器来包装协程。
1 def coroution(func): 2 def start(*args, **kwargs): 3 g = func(*args, **kwargs) 4 g.__next__() 5 return g 6 return start 7 8 9 # 使用这个装饰器就可以像下面这样编写和使用协程: 10 @coroution 11 def receive(): 12 print('Ready to receive') 13 while True: 14 n = yield 15 print('Go %s' % n) 16 17 18 r = receive() 19 r.send('hello world') # 无需初始调用.next()方法
协程一般会不断地执行下去,除非被显式关闭或者自己退出。关闭后如果继续给协程发送值就会引发StopIteration异常。正如前面关于生成器的内容中讲到的那样,close()操作将在协程内部引发GeneratorExit异常。
可以使用throw(exctype [, value [.tb]])方法在协程内部引发异常,其中exctype是指异常类型,value是指异常的值,而tb是指跟踪对象例如:
r.throw(RuntimeError, "You're hosed")
以这种方式引发的异常将在协程中当前执行的yield语句处出现。协程可以选择捕捉异常并以正确方式处理它们。使用throw()方法作为给协程的异步信号并不安全--永远都不应该通过单独的执行线程或信号处理程序调用这个方法。
如果yield表达式中提供了值,协程可以使用yield语句同时接收和发出返回值,例如:
def line_splitter(delimiter=None): print("Ready to split") result = None while True: line = yield result result = line.split(delimiter) l = line_splitter(',') l.__next__() print(l.send("a,b,c"))
首个__next__()调用让协程向前执行到yield result,这将返回result的值None。在接下来的send()调用中,接收到的值被放在line中并拆分到result中。
send()方法的返回值就是传递给下一条yield语句的值。换句话说,send()方法的返回值来自下一个yield表达式,而不是接收send()传递的值的yield表达式。
如果协程返回值,需要小心处理使用throw()引发的异常。如果使用throw()在协程中引发一个异常,传递给协程中下一条yield语句的值将作为throw()
方法的结果返回。如果需要这个值却又忘记保存它,它就会消失不见。
yield from
yield from 是在Python3.3才出现的语法,后面需要加的是可迭代对象。
a = 'qwertt' def str_to_list(): yield from a def str_to_list2(): for i in a: yield i print(list(str_to_list())) print(list(str_to_list2()))
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样两者可以直接发送和产出值,还可以传入异常
而不用在位于中间的协程中添加大量处理异常的样板代码。
双向通道: 调用方通过send()直接发送信息给子生成器,而子生成器yield的值,也直接返回给调用方
从Python 3.5开始引入了新的语法 async
和 await
,而await替代的就是yield from