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 i

def gen_from(*args, **kw):        
        for item in args:
                yield from item

new_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 都会抛 StopIteration

def 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)异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容