Python协程

目录:
一、基于生成器的协程
二、协程状态
三、协程预激装饰器
四、终止协程和异常处理
五、协程返回值
六、yield from
七、greenlet协程
八、gevent协程

Python并发之协程

协程(coroutine)可以在执行期间暂停(suspend),这样就可以在等待外部数据处理完成之后(例如,等待网络I/O数据),从之前暂停的地方恢复执行(resume)

一、基于生成器的协程

  • 生成器
    2001年,Python 2.2 通过了 PEP 255 -- "Simple Generators" ,引入了 yield 关键字实现了生成器函数。

yield item:yield包含”产出“和”让步“两个含义。生成器中 yield x 这行代码会 产出 一个值,提供给 next(...) 的调用方。此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用 next(...)。

  • 协程
    2005年,Python 2.5 通过了 PEP 342 -- "Coroutines via Enhanced Generators",给生成器增加了.send()、.throw()和.close()方法,第一次实现了基于生成器的协程函数(generator-based coroutines)。

send(item):生成器的调用方可以使用send()方法发送数据,发送的数据会成为生成器中yield表达式的值。此时称生成器为协程。协程指与调用方协作的过程,产出由调用方提供的值。

示例:

def simple_coroutine():
    print("->协程开始")
    x = yield
    print("->协程收到x:", x)


my_coro = simple_coroutine()
print(my_coro)
print(next(my_coro))
my_coro.send(50)
运行结果

运行结果解释:

① yield在表达式中的使用:如果协程只需从客户那里接收数据,那么产出的值是None(这个值是隐式指定的,因为yield关键字右边没有表达式)。
② 首先要需要调用next(...)。因为生成器还没启动,未在yield语句处暂停,所以一开始无法发送数据。
③ 调用send()方法后,协程定义体中的yield表达式会计算出50。调用send()方法后协程会恢复,一直运行到下一个yield表达式,或者终止。

二、协程状态

GEN_CREATED:创建状态(等待开始执行)
GEN_SUSPENDED:挂起状态(在yield表达式处暂停)
GEN_RUNNING:运行状态(正在被解释器执行。只有在多线程应用中才能看到这个状态)
GEN_CLOSED:关闭状态(执行结束)

协程激活方法

  • 1.调用next():next(generator)
  • 2.发送None:generator.send(None)

示例:

In[1]:  from inspect import getgeneratorstate
        def simple_coroutine():
            print("->协程开始")
            x = yield
            print(getgeneratorstate(my_coro2))
            print("->协程收到x:", x)

        my_coro = simple_coroutine()
        next(my_coro)
        print(getgeneratorstate(my_coro))
        my_coro.send(50)
In[2]:  print(getgeneratorstate(my_coro))

三、协程预激装饰器

1.@wraps装饰器

Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变)。为了消除这样的影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和docstring。

示例:

from functools import wraps
def my_decorater(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """
        wrapper.doc
        :param args:
        :param kwargs:
        :return:
        """
        print("calling decorater start")
        func(*args, **kwargs)
        print("calling decorater end")

    return wrapper


@my_decorater
def exam():
    """
    exam.doc
    :return:
    """
    print("example....")


exam()
print("func_name:", exam.__name__)
print("func_doc:", exam.__doc__)
运行结果

2.协程预激装饰器的实现

from functools import wraps
def coroutine_activator(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        generator = function(*args, **kwargs)
        next(generator)
        return generator

    return wrapper

示例:

def coroutine_activator(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        generator = function(*args, **kwargs)
        next(generator)
        return generator

    return wrapper


@coroutine_activator
def averager():
    total = 0.0
    count = 0
    average = 0.0

    while True:
        term = yield average
        total += term
        count += 1
        average = total / count


cor_avg = averager()
print(getgeneratorstate(cor_avg))
运行结果

四、终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next()函数或.send()方法的调用方(即触发协程的对象)。

1.generator.throw(exc_type[, exc_value[, traceback]])

  • 1.该方法会使生成器在暂停的yield表达式处抛出指定的异常。
  • 2.如果生成器处理了该异常,代码会继续执行到下一个yield表达式,而产出的值会成为generator.throw()方法的返回值。
  • 3.如果没有处理这个异常,异常则会向上冒泡。

示例:

class DemoException(Exception):
    pass


def demo_exc_handling():
    print("-->coroutine started")

    while True:
        try:
            x = yield
#         except GeneratorExit:
#             print("-->GeneratorExit has been handled. Call close methond...")
        except DemoException:
            print("-->DemoException has been handled. Continuing...")
        else:
            print("-->coroutine received:{!r}".format(x))
        finally:
            print("-->end...")


exc_coro = demo_exc_handling()
next(exc_coro)
print(getgeneratorstate(exc_coro))

exc_coro.throw(DemoException)
print(getgeneratorstate(exc_coro))

exc_coro.throw(ZeroDivisionError)
print(getgeneratorstate(exc_coro))
运行结果

2.generator.close()

  • 1.该方法会使生成器在暂停的yield表达式处抛出GeneratorExit异常。
  • 2.如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错。
  • 3.如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。
  • 4.生成器抛出的其他异常会向上冒泡,传给调用方。
coro = demo_exc_handling()
next(coro)
coro.send(10)

coro.close()
coro.send(20)
运行结果

五、协程return值

有些协程在被激活后,每次驱动(drive)协程时,不会产出值,而是在最后(协程正常终止时)返回一个值(通常是某种累加值)。

from collections import namedtuple
Result = namedtuple('Result', 'count average')
def averager():
    total = 0.0
    count = 0
    average = 0.0

    while True:
        term = yield average
        # 为了返回值,协程必须正常终止。这里使用条件判断,以便退出累计循环
        if term is None:
            break
        total += term
        count += 1
        average = total / count
    # 返回一个 namedtuple,包含 count 和 average 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误
    return Result(count, average)


coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)

#捕获StopIteration异常,获取 averager 返回的值:
try:
    coro_avg.send(None)
except StopIteration as exc_data:
    r = exc_data.value
print(r)
运行结果

六、yield from

  • 在生成器gen中使用yield from subgen()时,子生成器subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
  • yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。通过这个结构,协程可以把功能委托给子生成器。
  • 调用方:调用委派生成器的客户端代码。
  • 委派生成器:包含 yield from <iterable> 表达式语句的生成器函数(包含yield from语句的生成器)。
  • 子生成器:从 yield from 表达式中 <iterable>部分获取的生成器(yield from后面的表达式)。

简单示例:

def gen():
    yield from "AB"
    yield from range(1, 3)
    
    
print(list(gen()))
运行结果

示例:

Result = namedtuple('Result', 'count average')


# 子生成器
def averager():
    total = 0.0
    count = 0
    average = 0.0

    while True:
        term = yield average
        if term is None:
            break
        total += term
        count += 1
        average = total / count

    return Result(count, average)


# 委派生成器
def grouper(results, key):
    while True:
        results[key] = yield from averager()


# 客户端代码
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        # 激活委派生成器
        next(group)

        for value in values:
            group.send(value)

        group.send(None)

    print(results)


data = {"girls:kg": [49.9, 39.5, 44.3, 44.2, 50.3, 40.5, 40.6, 30.8, 50, 55.5],
        "girls:m": [1.6, 1.5, 1.4, 1.3, 1.6, 1.42, 1.55, 1.66, 1.43, 1.60],
        "boys:kg": [43.9, 48.5, 44.3, 44.2, 50.3, 40.5, 40.6, 30.8, 50, 55.5],
        "boys:m": [1.6, 1.4, 1.4, 1.3, 1.6, 1.42, 1.55, 1.66, 1.43, 1.60],
        }
main(data)
运行结果

运行结果解释:

委派生成器在yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复运行。

注意:

  • 如果子生成器不终止,委派生成器会在yield from处永远暂停。
  • 因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个子生成器,以此类推。最终,这个链条要以一个只使用yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束。
  • 任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(...)函数或.send(...)方法。

yield from的意义

  • 1.子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 2.使用send()方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 3.生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr)异常抛出。
  • 4.yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
  • 5.传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器。
  • 6.如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。

七、greenlet协程

greenlet由来

虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。 与此同时,Python的一个衍生版 Stackless Python。实现了原生的协程,它更利于使用。 于是,大家开始将 Stackless 中关于协程的代码 。单独拿出来做成了CPython的扩展包。 这就是 greenlet 的由来,因此 greenlet 是底层实现了原生协程的 C扩展库。

import greenlet

# 创建greenlet协程
coroutine = greenlet.greenlet() 
# 运行greenlet协程
coroutine.switch()

示例:生产者-消费者模式

import greenlet
import random
def producer():
    while True:
        item = random.randint(0, 99)
        print("生成数字{}".format(item))
        c.switch(item)          # 将data传给c,并切换到c

def consumer():
    while True:
        item = p.switch()       # 切换到p,等待传入数值
        print("消费数字{}".format(item))


if __name__ == "__main__":
    c = greenlet.greenlet(consumer)
    p = greenlet.greenlet(producer)
    c.switch()
运行结果

greenlet 的优势:

  • 1.高性能的原生协程。
  • 2语义更加明确的显式切换。
  • 3.直接将函数包装成协程,保持原有代码风格。

八、gevent协程

gevent是什么:http://www.gevent.org/

gevent是第三方库,通过greenlet实现协程,其基本思想是:

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。

import gevent
# 创建协程
gevent.spawn(参数)
# gevent协程运行列表
gevent.joinall([协程列表])  

示例:生产者-消费者模式

import gevent
from gevent.queue import Queue
import random


def Consumer(queue):
        while True:
            item = queue.get()
            print("消费数字{}".format(item))


def Producer(queue):
        while True:
            item = random.randint(0, 99)
            queue.put(item)
            print("生产数字{}".format(item))


queue = Queue(3)

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