美文网首页Python
Python3中yield与yield from详解

Python3中yield与yield from详解

作者: dingxutao | 来源:发表于2021-11-29 13:13 被阅读0次

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

    相关文章

      网友评论

        本文标题:Python3中yield与yield from详解

        本文链接:https://www.haomeiwen.com/subject/vugcxrtx.html