• Python协程详解(一)


    yield有两个意思,一个是生产,一个是退让,对于Python生成器的yield来说,这两个含义都成立。yield这个关键字,既可以在生成器中产生一个值,传输给调用方,同时也可以从调用方那获取一个值,在生成器内部使用。此外,yield还会作出让步,暂停生成器,让调用方继续工作,直到调用方需要下一个数据时,调用方则陷入等待直到成器提供给调用方所需的数据,如此循环往复。乍一听,有点像多线程,不明白多线程的同学也不要紧张,可以简单的解释一下多线程

    解释多线程之前,我们先解释一下进程,进程可以看成是电脑里运行的一个实例,比方说,我运行一个浏览器,是一个进程,运行QQ,同样也是进程,我用浏览器浏览网站,用浏览器听音乐和下载东西,可以看成浏览器这个进程,里面有3个线程同时在为我做浏览网站,播放音乐还有下载文件。而我用QQ和人聊天,同时我又用QQ给人传输文件,同样也是在QQ这个进程中,有两个线程在为我传输聊天内容,同时传输文件。当然,上述说的并不严谨,只是为了好理解,因为对于像浏览器或者QQ这样的进程,每时每秒可能有成百上千的线程在运行,有可能记录日志或者其他

    而协程相比于线程,最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显

    下面用个例子来看一下协程的运作:

    def simple_coroutine():
        for i in range(3):
            x = yield i + 1  # <1>
            print("从调用方获取的值:%s" % x)
    
    
    my_coro = simple_coroutine()  # <2>
    first = next(my_coro)  # <3>
    for i in range(5):  # <4>
        try:
            y = my_coro.send(i)  # <5>
            print("从生成器中获取的值:%s" % y)
        except StopIteration:
            print("生成器的值拉取完毕")  # <6>
    print("生成器最初获取的值:%s" % first)
    

      

    运行结果:

    从调用方获取的值:0
    从生成器中获取的值:2
    从调用方获取的值:1
    从生成器中获取的值:3
    从调用方获取的值:2
    生成器的值拉取完毕
    生成器的值拉取完毕
    生成器的值拉取完毕
    生成器最初获取的值:1

      

    我们先来说一下程序的运行过程,先看程序中<2>处的代码,传统的概念中,我先执行了my_coro = simple_coroutine() 这块代码,所以会理所当然的认为, simple_coroutine() 这个方法要先执行完毕才能接着执行后续的代码,但实际上不是,因为yield会标明这个方法是一个生成器,所以在程序的最初,他不会先执行完毕 simple_coroutine() 方法,而是把my_coro 这个变量声明称一个生成器,跳过simple_coroutine() ,接着执行后续代码

    Python将my_coro 声明称一个生成器后,调用了<3>处的next(my_coro) ,这个方法才开始会顺序执行simple_coroutine()方法中的代码,在Python解释器执行simple_coroutine()方法时,遇到yield关键字,生成器将会陷入等待,这时候解释器会跳到生成器之外,也就是<3>之后的代码,我们调用生成器的send()方法,并传输一个值,这时候Python解释器会从外部的代码重新跳回simple_coroutine() 方法,中在之前停留的地方继续执行

    他会将外部传来的值赋给x变量,并顺序执行,直到遇到下一个yield,再像之前那样跳出方法外

    由于生成器能提供的值有限,所以当simple_coroutine()方法中执行了3次循环,生成器已经没有多余的值可供调用方获取了,所以每次调用生成器的send()方法,都会抛出StopIteration异常

    这里有一点要注意,要激活一个生成器,一定要调用next()方法,而不是调用生成器的send()方法,如果直接调用send()方法会报错

    协程有四种状态,分别是

    GEN_CREATED:等待执行

    GEN_RUNNING:解释器执行

    GEN_SUSPENDED:在yield表达式处暂停

    GEN_CLOSED:执行结束

    协程的状态可以用inspect.getgeneratorstate()函数来确定,来看下面的例子:

    from inspect import getgeneratorstate
    from time import sleep
    import threading
    
    
    def get_state(coro):
        print("其他线程生成器状态:%s", getgeneratorstate(coro))  # <1>
    
    
    def simple_coroutine():
        for i in range(3):
            sleep(0.5)
            x = yield i + 1  # <1>
    
    
    my_coro = simple_coroutine()
    print("生成器初始状态:%s" % getgeneratorstate(my_coro))  # <2>
    first = next(my_coro)
    for i in range(5):
        try:
            my_coro.send(i)
            print("主线程生成器初始状态:%s" % getgeneratorstate(my_coro))  # <3>
            t = threading.Thread(target=get_state, args=(my_coro,))
            t.start()
        except StopIteration:
            print("生成器的值拉取完毕")
    print("生成器最后状态:%s" % getgeneratorstate(my_coro))  # <4>
    

      

    执行结果:

    生成器初始状态:GEN_CREATED
    生成器状态:%s GEN_SUSPENDED
    生成器状态:%s GEN_SUSPENDED
    生成器的值拉取完毕
    生成器的值拉取完毕
    生成器的值拉取完毕
    生成器最后状态:GEN_CLOSED
    

        

    <2>处,在激活协程之前,协程的状态是GEN_CREATED,而执行next()之后,以及在调用生成器send()之间,我分主线程也就是调用方和多线程去观察协程的状态,结果状态都是GEN_SUSPENDED,也就是协程处于暂停的状态,我原本想用多线程去捕捉协程的运行态,结果即便是多线程捕捉协程也是GEN_SUSPENDED,而GEN_RUNNING也说明,只有带解释器在运行协程的时候,协程的状态才是GEN_RUNNING,最后是GEN_CLOSED,我们拉取完协程的值后,协程的状态就变为执行结束

    示例:使用协程计算平均值

    我们可以开发一个协程,不断的往协程发送值,并且让协程累计之前的值并计算平均值,如下:

    from functools import wraps
    
    
    def coroutine(func):
        @wraps(func)
        def primer(*args, **kwargs):
            gen = func(*args, **kwargs)
            next(gen)
            return gen
    
        return primer
    
    
    @coroutine  # <1>
    def averager():
        total = .0
        count = 0
        average = None
        while True:
            term = yield average
            total += term
            count += 1
            average = total / count
    
    
    try:
        coro_avg = averager()
        print(coro_avg.send(10))
        print(coro_avg.send(20))
        print(coro_avg.send(30))
        coro_avg.close()  # <2>
        print(coro_avg.send(40))
    except StopIteration:
        print("协程已结束")
    

        

    运行结果:

    10.0
    15.0
    20.0
    协程已结束
    

        

    在<1>处,我们用一个装饰器来预先激活协程,而不是之后再调用方里执行一个next()函数。然后,我们不断往协程里传10、20、30,而协程不断累计传入的值,并计算所有值的平均值返回给调用方,最后,我们在<2>处调用协程的close()函数,关闭协程,再调用send()方法,会发现抛出StopIteration异常

    当发送给协程不是数字,会导致协程内部有异常抛出

    for i in range(1, 6):
        try:
            print(coro_avg.send(i))
            if i % 3 == 0:
                coro_avg.send('')
        except StopIteration:
            print("协程已结束")
        except TypeError:
            print("传入值异常")
    

      

    运行结果:

    1.0
    1.5
    2.0
    传入值异常
    协程已结束
    协程已结束
    

      

      

    我们设置,当i为3的时候,多传入一个空字符串,结果协程抛出类型错误,协程将运行状态改为结束,之后再往协程传值,都抛出StopIteration异常

     我们可以让协程处理一些特定的异常,比如:

    class DemoException(Exception):  # <1>
        pass
    
    
    def demo_exec_handling():
        print("coroutine started")
        while True:
            try:
                x = yield  # <2>
            except DemoException:  # <3>
                print("DemoException handled")
            else:
                print("coroutine received:{}".format(x))

      

    运行结果

    >>> exec_coro = demo_exec_handling()
    >>> next(exec_coro)
    coroutine started
    >>> print(exec_coro.send(1))
    coroutine received:1
    None
    >>> exec_coro.send(2)
    coroutine received:2
    >>> exec_coro.send(3)
    coroutine received:3
    >>> exec_coro.throw(DemoException)
    DemoException handled
    >>> exec_coro.send(4)
    coroutine received:4
    >>> exec_coro.throw(ZeroDivisionError)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 5, in demo_exec_handling
    ZeroDivisionError
    >>> exec_coro.send(5)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

        

    <1>处的DemoException是用来测试协程的,首先我们可以看到,当yield右边没有任何式子时,返回给调用方的是一个None对象,其次如果我们调用throw()方法将异常传入协程,因为协程里有关于DemoException的捕捉,所以协程会继续执行,当我们继续传入ZeroDivisionError,则协程结束

    让协程返回值

    我们可以改造之前的averager()函数,使它可以返回一个对象,对象里有count和average两个属性

    from collections import namedtuple
    
    Result = namedtuple("Result", ["count", "average"])
    
    
    def averager():
        total = .0
        count = 0
        average = None
        while True:
            term = yield average
            if term is None:
                break
            total += term
            count += 1
            average = total / count
        return Result(count, average)
      
    
    运行结果:
    
    >>> coro_avg = averager()
    >>> next(coro_avg)
    >>> coro_avg.send(10)
    10.0
    >>> coro_avg.send(20)
    15.0
    >>> coro_avg.send(30)
    20.0
    >>> coro_avg.send(None)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: Result(count=3, average=20.0)
    

        

    当我们发送None的时候,协程结束,返回结果,一如既往,生成器会抛出StopIteration异常,异常对象的value属性保存着返回值,为了获取返回值,我们还要再修改一下代码

    try:
        coro_avg = averager()
        next(coro_avg)
        coro_avg.send(10)
        coro_avg.send(20)
        coro_avg.send(30)
        coro_avg.send(None)
    except StopIteration as exc:
        result = exc.value
    print(result)
    

        

    运行结果:

    Result(count=3, average=20.0)
    

        

    结语:关于协程yield结构这一块,到此暂做结束,下一章会介绍协程的yield from结构,yield from结构会在内部自动捕获StopIteration异常,还会把协程的返回值变成yield from表达式的值,下一章节将会讨论yield from的结构和用法,谢谢大家

  • 相关阅读:
    30岁的程序猿坐的太久,也要用一下脑子
    HIPO图
    CMake入门(二)
    hdu1711 Number Sequence
    EF架构~在ef中支持IQueryable级别的Contains被翻译成了Exists,性能可以接受!
    JS框架~Angularjs
    将不确定变为确定~transactionscope何时提升为分布式事务?(sql2005数据库解决提升到MSDTC的办法)
    SignalR实现服务器与客户端的实时通信
    基础才是重中之重~LazyInitializer.EnsureInitialized对属性实现化的性能优化
    [置顶] 电视机顶盒搜台原理和方法简析
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9064152.html
Copyright © 2020-2023  润新知