Python3中yield与yield from详解
一、yield
学习协程的第一门课程,是要认识生成器,有了生成器的基础,才能更好地理解协程。
如果你是新手,那么你应该知道迭代器,对生成器应该是比较陌生的吧。没关系,看完这系列文章,你也能从小白成功过渡为Ptyhon高手。
本文主要从以下几个方面来学习yield的知识点:
1:可迭代、迭代器、生成器
2:如何运行/激活生成器
3:生成器的执行状态
4:从生成器过渡到协程:yield
1:可迭代、迭代器、生成器
我们如何区分区分一个对象是否是可迭代、迭代器、还是生成器呢?有一个简单的办法:
from collections.abc import Iterable, Iterator, Generator
isinstance(obj, Iterable) # 可迭代对象
isinstance(obj, Iterator) # 迭代器
isinstance(obj, Generator) # 生成器
Iterable:一般在python中想字符串,list, dict, tuple, set, deque等都是可迭代对象,从表象上看他们都可以使用 for 来循坏迭代,但实际上他们并不是迭代器,也不是生成器。因为一个对象只要实现了__iter__ 方法的,均可称为可迭代对象。
扩展知识:
可迭代对象,是其内部实现了,__iter__ 这个魔术方法。
可以通过,dir()方法来查看是否有__iter__来判断一个变量是否是可迭代的。
Iterator:迭代器,一般对象只要实现了__next__ 与 __iter__ 方法的均可称为生成器对象,因为它可以不用for循序来间断的获取元素值(next(obj)).
迭代器,是在可迭代的基础上实现的。要创建一个迭代器,我们首先,得有一个可迭代对象。
注意:迭代器在元素值迭代结束的时候会抛出 StopIteration 异常,这是必要的。s = "1234abc"
iterator = iter(s)
isinstance(iterator , Iterator) # True
扩展知识:
迭代器,是其内部实现了,__next__、__iter__ 这个魔术方法。(Python3.x)
可以通过,dir()方法来查看是否有__next__来判断一个变量是否是迭代器的。
Generator:生成器,是在迭代器的基础上(可以用for循环,可以使用next()),再实现了yield。
yield 是什么东西呢,它相当于我们函数里的return。在每次next(),或者for遍历的时候,都会yield这里将新的值返回回去,并在这里阻塞,等待下一次的调用。正是由于这个机制,才使用生成器在Python编程中大放异彩。实现节省内存,实现异步编程
实现生成器的方法:
(1): 使用列表生成式
# 使用列表生成式,注意不是[],而是()
L = (x * x for x in range(10))
print(isinstance(L, Generator)) # True
(2): 实现了yield的函数
from inspect import getgeneratorstate
def mygen(n):
now = 0
while now < n:
r = yield now
now += 1
raise StopIteration
StopIteration:在生成器工作过程中,若生成器不满足生成元素的条件,就会抛出异常StopIteration,也应该抛出该异常。
注意:
(1): 一般使用for来循环迭代生成器,在生成器结束是python解释器会在for结束后自动捕获StopIteration异常,让我们的程序没有感知(2): 使用next(gen), 当next最后一个一个yield后,无论后面yield后面有没有return都会抛出StopIteration; 那么此时如何获取生成器函数的返回值呢?你只需要在最后一次的next(gen),使用try...except StopIteration as e即可, 返回值在e.value中。
try:
ret = next(gtw)
except StopIteration as e:
print("GGG:", e.value) # 函数没有返回值,默认None
send(param): 当生成器使用send(param)是,注意以下部分:
a: gen.send(None),相当于next(next), 因为next就是不带参数,默认是send(None)
b: 在gen.close或者抛出StopIteration 之前使用gen.send(100) 或 gen.send("abc")
r = yield now
此时r的值就是send发送的值。执行流程如下:
(1): gen = mygen
(2): print(next(gen) ) # 此时执行到r = yield now,在yield now时,print打印的值为0,生成器暂停并阻塞在yield处, now + 1 该处代码不会执行,因为暂停并阻塞了
(3): print(gen.send(100)) # 此时r = yield now,会先接收到send的参数值,r就是参数的值,程序将会恢复执行yiled后面的代码,直到再次遇到下一个yield , 此时print打印的值为1,程序再次会暂停并阻塞。注意:send在上一次yield暂停阻塞处,yield会先接收send的参数值,然后恢复执行后面的程序,直到下一个yield
可迭代象和迭代器,是将所有的值都生成存放在内存中,而生成器则是需要元素才临时生成,节省时间,节省空间。
2:如何运行/激活生成器
由于生成器并不是一次生成所有元素,而是一次一次的执行返回,那么如何刺激生成器执行(或者说激活)呢?激活主要有两个方法:
a: 使用next() # 相当于gen.send(None) , 第一次启动、激活只能是send(None) , send不能是其他函数
b: 使用generator.send(None)
3: 生成器的执行状态
from inspect import getgeneratorstate, isgeneratorfunction
使用inspect.getgeneratorstate就能判断生成器的状态,一般在其生命周期中,会有如下四个状态:
GEN_CREATED # 等待开始执行
GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)GEN_SUSPENDED # 在yield表达式处暂停
GEN_CLOSED # 执行结束>>> gen = mygen(2)
>>> print("1:", getgeneratorstate(gen)) # GEN_CREATED
>>> print(next(gen)) # print(gen.send(None))
>>> print("2:", getgeneratorstate(gen)) # GEN_SUSPENDED
>>> gen.close()
>>> print("3:", getgeneratorstate(gen)) # GEN_CLOSED
4: 从生成器过渡到协程:yield
通过上面的介绍,我们知道生成器为我们引入了暂停函数执行(yield)的功能。当有了暂停的功能之后,人们就想能不能在生成器暂停的时候向其发送一点东西(其实上面也有提及:send(None))。这种向暂停的生成器发送信息的功能通过 PEP 342 进入 Python 2.5 中,并催生了 Python 中协程的诞生。
注意从本质上而言,协程并不属于语言中的概念,而是编程模型上的概念。
协程和线程,有相似点,多个协程之间和线程一样,只会交叉串行执行;也有不同点,线程之间要频繁进行切换,加锁,解锁,从复杂度和效率来看,和协程相比,这确是一个痛点。协程通过使用 yield 暂停生成器,可以将程序的执行流程交给其他的子程序,从而实现不同子程序的之间的交替执行。
def jumping_range(N):
index = 0 while index < N:
# 通过send()发送的信息将赋值给
jump jump = yield index
if jump is None:
jump = 1
index += jump
if __name__ == '__main__':
itr = jumping_range(5)
print(next(itr)) # 0
print(itr.send(2)) # 2
print(next(itr)) # 3
print(itr.send(-1)) # 2
这里解释下为什么这么输出。
重点是jump = yield index这个语句。
分成两部分:
yield index 是将index return给外部调用程序。
jump = yield 可以接收外部程序通过send()发送的信息,并赋值给jump
以上这些,都是讲协程并发的基础必备知识,请一定要亲自去实践并理解它,不然后面的内容,将会变得枯燥无味,晦涩难懂。
二、yield from
yield from 所在的函数被称为委托生成器,它主要为调用方和子生成器提供一个双向通道;那么下面我们你主要从以下方面来讲解yield from的相关知识:
1: 为什么要使用协程
2: yield from的用法详解
3: 为什么要使用yield from
1: 为什么要使用协程
在使用yield from之前,请读者把上面的yield的知识好好复习巩固一下。
总的来说asyncio比线程优越的地方就是:协程不像线程那样需要频繁进行上下文切换、加锁、解锁,这些过程,所以协程之间切换的时间开销将大幅减小,效率上将大幅提高。对于爬虫、读写文件、读磁盘等这种非常耗时的IO来说更是如此
def spider_xx(url):
html = get_html(url)
......
data = parse_html(html)
我们都知道,get_html()等待返回网页是非常耗IO的,一个网页还好,如果我们爬取的网页数据极其庞大,这个等待时间就非常惊人,是极大的浪费。
聪明的程序员,当然会想如果能在get_html()这里暂停一下,不用傻乎乎地去等待网页返回,而是去做别的事。等过段时间再回过头来到刚刚暂停的地方,接收返回的html内容,然后还可以接下去解析parse_html(html)。
利用常规的方法,几乎是没办法实现如上我们想要的效果的。所以Python想得很周到,从语言本身给我们实现了这样的功能,这就是yield语法。可以实现在某一函数中暂停的效果。
试着思考一下,假如没有协程,我们要写一个并发程序。可能有以下问题
1)使用最常规的同步编程要实现异步并发效果并不理想,或者难度极高。
2)由于GIL锁的存在,多线程的运行需要频繁的加锁解锁,切换线程,这极大地降低了并发性能;
而协程的出现,刚好可以解决以上的问题。它的特点有
协程是在单线程里实现任务的切换的
利用同步的方式去实现异步
不再需要锁,提高了并发性能
2:yield from的用法
yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
astr='ABC' # 字符串
alist=[1,2,3] # 列表
adict={"name":"wangbm","age":18} # 字典
agen=(i for i in range(4,8)) # 生成器
def gen(*args, **kw):
for item in args:
for i in item:
yield idef gen_from(*args, **kw):
for item in args:
yield from itemnew_list=gen(astr, alist, adict, agen)
print(list(new_list)) # ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]new_gen_list=gen_from(astr, alist, adict, agen)
print(list(new_gen_list)) # ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
当然上面只是小case, yield from的应用远不仅仅如此。当 yield from 后面加上一个生成器后,就实现了生成的嵌套。
当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现,讲解它之前,首先要知道这个几个概念:
1、调用方: 调用委托生成器的客户端(调用方)代码
2、委托生成器: 包含yield from表达式的生成器函数
3、子生成器: yield from 后面加的生成器函数
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。
所谓的双向通道是什么意思呢?调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。
你可能会经常看到有些代码,还可以在yield from前面看到可以赋值。这是什么用法?
你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
from collections import namedtuple
Result = namedtuple('Result', 'count average')def get_average():
""" 子生成器 """
total = 0.0
count = 0
average = None
while True:
# send 发送值给yield接收, yield 后面可以没有参数;
# 有参数时 yield average 是为了让调用方迭代获取a值,和 term 没有关系
term = yield average
if term is None:
break total += term
count += 1
average = total / count
return Result(count, average)def delegate_gen(results, key):
""" 委托生成器 """
while True:
# 只有当生成器 get_average()结束,才会返回结果给results赋值
# 无 while True 抛 StopIteration print("grouper end")
results[key] = yield from get_average()
# return results # 有无 while True 都会抛 StopIterationdef call_main(data):
""" 调用方 """
results = {}
for key, values in data.items():
delegation = delegate_gen(results, key)
next(delegation) # 启动/激活子生成器,第一次运行到 yield 阻塞暂停
for value in values:
delegation.send(value)
delegation.send(None) # 结束子生成器(return 了)
print(results)
代码里面有几个很重要的点,作如下讲解:
1:启动/激活子生成器,next(delegation) 与 delegation.send(None), send参数只能是None
2:yield from 对【调用方】与【子生成器】起到双向通道的作用
3:子生成器结束时,子生成器的返回值为默认值或是其他,都会抛出 StopIteration 异常,但是yield from会自动处理子生成器的该异常,那么ret = yield from delegate_gen(...) 中, ret就是子生成器gen()的返回值, 等价于:
try:
delegation.send(None)
except StopIteration as e:
ret = e.value
4: 关于委托生成器抛出 StopIteration 异常的说明:
(1):yield from 【在】while True 里,当子生成器结束后,并接收到子生成器的返回值后,委托生成器【不会】再次抛出 StopIteration, 代码如下:
while True:
yield from get_average()
(2): 如果yield from 【不在】while True 里,当子生成器结束后,并接收到子生成器的返回值后, 委托生成器【会】再次抛出 StopIteration, 代码如下:
yield from get_average()
(3): 只要yield from 【不在】while True 里,当子生成器结束后,并接收到子生成器的返回值后, 无论委托生成器函数有无return(无return, 默认None)都【会】抛出 StopIteration
关于 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
以上的代码,稍微有点复杂,有兴趣的同学可以结合以下说明去研究看看。
1: 迭代器(即可指子生成器)产生的值直接返还给调用者
2: 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
3: 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
4: 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
5: 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
6: 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
7: 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。