yield与生成器
前面我们介绍过生成器:迭代器、可迭代对象、生成器的区别和联系
使用了 yield 的函数被称为生成器(generator)。
他最大的特点是:在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
生成器来实现斐波那契数列:
def fib(stop):
n, a, b = 1, 0, 1
while n < stop:
yield b
a, b = b, a + b
n += 1
return "没有数据了"
f = fib(10) # f 是一个迭代器,由生成器返回生成
while True:
print(next(f), end=" ")
# 当迭代遇到没有元素可取,也会抛出StopIteration异常
send
send方法 可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置。
def gen_func():
# 1.下面这段代码可以:产出值,接收值
# html变两用来接受send函数传进来的值
html = yield "http://xxx.com"
print(f"html:{html}")
yield 1
yield 2
yield 3
return "aa"
if __name__ == "__main__":
# 启动生成器由两种方法,next(),send()
gen = gen_func()
# 调用next函数会执行第一个next,并把yield后的值返回,并暂停执行
url = next(gen)
print(url) # http://xxx.com
# 调用send方法,会把send的值传入生成器,被函数内部的html变量接受了
# 并会执行到下一个yield,yield后的1被下面的aa变量接受了再次暂停
aa = gen.send("html")
print(f"aa:{aa}")
# 结果如下:
# http://xxx.com
# html:html
# aa:1
如果send方法没传入值,内部print的html为None,send的返回值为yield的值。
注意第一次调用send时,只能send一个None,因为一开始还没运行到yield,必须先启动一次生成器,第一种是send(None),第二种调用next():
def gen_func():
# 1.下面这段代码可以:产出值,接收值
html = yield "http://xxx.com"
print(html)
yield 1
yield 2
yield 3
return "aa"
if __name__ == "__main__":
# 启动生成器由两种方法,next(),send()
gen = gen_func()
url = gen.send("html")
print(url)
# TypeError: can't send non-None value to a just-started generator
正确如下:
def gen_func():
# 1.下面这段代码可以:产出值,接收值
html = yield "http://xxx.com"
print(html)
yield 1
yield 2
yield 3
return "aa"
if __name__ == "__main__":
# 启动生成器由两种方法,next(),send()
gen = gen_func()
# 不能第一次就send一个非None的值,send(None)或next(gen)
url = gen.send(None)
print(url)
# send 可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置
url = gen.send("html")
print(url)
# http://xxx.com
# html
# 1
close
close用来关闭生成器,关闭后,再次调用next时,抛出StopIteration:
def gen_func():
# 1.下面这段代码可以:产出值,接收值
yield "http://xxx.com"
yield 1
yield 2
yield 3
return "aa"
if __name__ == "__main__":
gen = gen_func()
print(next(gen))
gen.close()
next(gen)
# 最后一次调用next时,抛出StopIteration
throw
在yield处抛出异常:
def gen_func():
yield "http://xxx.com"
yield 2
yield 3
return "aa"
if __name__ == "__main__":
gen = gen_func()
print(next(gen))
gen.throw(Exception, "download error") # 实际是在 yield "http://xxx.com"出抛异常
print(next(gen))
# http://xxx.com
# in gen_func yield 3, Exception: download error
yield from
在python3.3 新加的语法。
看下面一个例子:
from itertools import chain
my_list = [1, 2, 3]
my_dict = {"a":1, "b":2}
for val in chain(my_list, my_dict, range(5,9)):
print(val)
上面例子主要是使用itertools工具包的chain方法把两个字典链接到一起了。
如果通过yield from
也一样可以实现上面的效果:
my_list = [1, 2, 3]
my_dict = {"a": 1, "b": 2}
def my_chain(*args, **kwargs):
"""这个方法用来把若干个可迭代对象通过yield from返回"""
for my_iterable in args:
yield from my_iterable
for val in my_chain(my_list, my_dict, range(5, 9)):
print(val)
yield from
后面一般是一个迭代器或生成器,作用就是把课迭代对象的每个元素通过yield
返回。同样的,含有yield from
的函数返回的也是一个生成器。
通过yield from
可以实现委派模式:
- 调用方:调用委派生成器的客户端(调用方)代码
- 委托生成器:包含yield from表达式的生成器函数
- 子生成器:yield from后面加的生成器函数
如下,利用委派模式求平均数:
# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
count += 1
total += new_num
average = total/count
# 委托生成器
def proxy_gen():
while True:
yield from average_gen()
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激下生成器
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
if __name__ == '__main__':
main()
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。
所谓的双向通道是什么意思呢?
调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。
# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
if new_num is None:
break
count += 1
total += new_num
average = total/count
# 每一次return,都意味着当前协程结束。
return total,count,average
# 委托生成器
def proxy_gen():
while True:
# 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
total, count, average = yield from average_gen()
print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激协程
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
calc_average.send(None) # 结束协程
# 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程
if __name__ == '__main__':
main()
运行后输出
10.0
15.0
20.0
计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0
yield from 还帮助我们做了很多异常处理:
#一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象
"""
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
总结
- 迭代器(即可指子生成器)产生的值直接返还给调用者
- 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
- 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
- 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
- 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
- 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
- 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。