Detailed usage and examples of Python coroutines

Syntactically, coroutines are similar to generators in that they are functions whose definition body contains the yield keyword. However, in coroutines, yield usually occurs on the right-hand side of an expression (for example, datum = yield) and can yield a value or not - if there is no expression after the yield keyword, the generator yields None .

The coroutine may receive data from the caller, but the caller provides the data to the coroutine using the .send(datum) method instead of the next(…) function.

The ==yield keyword can even not receive or send out data. Regardless of how the data flows, yield is a flow control tool that enables cooperative multitasking: coroutines can yield control to the central scheduler, thereby activating other coroutines == .

Basic behavior of coroutine generators

Here is the simplest coroutine code:

def simple_coroutine():
    print('-> start')
    x = yield
    print('-> recived', x)

sc = simple_coroutine()

next(sc)
sc.send('zhexiao')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Explanation: 
1. A coroutine is defined using a generator function: there is a yield keyword in the definition body. 
2. yield is used in expressions; if the coroutine only needs to receive data from the client, the value of the yield is None -- this value is implicitly specified because there is no expression to the right of the yield keyword. 
3. The next(…) function must be called first, because the generator has not been started yet, and has not been paused at the yield statement, so data cannot be sent at first. 
4. Call the send method, pass the value to the variable of yield, then the coroutine resumes, and continues to execute the following code until it runs to the next yield expression, or terminates.

==Note: the send method only works when the coroutine is in the GEN_SUSPENDED state, so we use the next() method to activate the coroutine to stop at the yield expression, or we can also use sc.send(None), the effect is the same as next(sc) is the same as ==.

The four states of the coroutine:

A coroutine can be in one of four states. The current state can be determined using the inspect.getgeneratorstate(…) function, which returns one of the following strings: 
1. GEN_CREATED: waiting to start execution 
2. GEN_RUNNING: interpreter is executing 
3. GEN_SUSPENED: paused at yield expression 
4. GEN_CLOSED: execution ends

== The first step to call the next(sc) function is usually called "prime" the coroutine== (ie, let the coroutine execute forward to the first yield expression, ready to be used as an active coroutine) .

import inspect

def simple_coroutine(a):
    print('-> start')

    b = yield a
    print('-> recived', a, b)

    c = yield a + b
    print('-> recived', a, b, c)

# run 
sc = simple_coroutine(5)

next(sc)
sc.send(6) # 5, 6
sc.send(7) # 5, 6, 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Example: Calculating Moving Average Using Coroutines

def averager():
    total = 0.0
    count = 0
    avg = None

    while True:
        num = yield avg
        total += num
        count += 1
        avg = total/count

# run
ag = averager()
# 预激协程
print(next(ag))     # None

print(ag.send(10))  # 10
print(ag.send(20))  # 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Explanation: 
1. After calling the next(ag) function, the coroutine will execute forward to the yield expression and output the initial value of the average variable - None. 
2. At this point, the coroutine pauses at the yield expression. 
3. Use send() to activate the coroutine, assign the sent value to num, and calculate the value of avg. 
4. Use print to print out the data returned by yield.

Termination of coroutines and exception handling

Unhandled exceptions in the coroutine bubble up and are passed to the caller of the next function or send method (ie the object that triggered the coroutine).

== One way to terminate the coroutine: send a sentinel value to make the coroutine exit. Built-in constants such as None and Ellipsis are often used as sentinel values ​​for ==.

Explicitly send exceptions to coroutines

Starting with Python 2.5, client code can call two methods on the generator object to explicitly send exceptions to the coroutine.

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

致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

generator.close()

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

异常处理示例:

class DemoException(Exception):
    """
    custom exception
    """

def handle_exception():
    print('-> start')

    while True:
        try:
            x = yield
        except DemoException:
            print('-> run demo exception')
        else:
            print('-> recived x:', x)

    raise RuntimeError('this line should never run')

he = handle_exception()
next(he)
he.send(10) # recived x: 10
he.send(20) # recived x: 20

he.throw(DemoException) # run demo exception

he.send(40) # recived x: 40
he.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

如果传入无法处理的异常,则协程会终止:

he.throw(Exception) # run demo exception
  • 1

yield from获取协程的返回值

为了得到返回值,协程必须正常终止;然后生成器对象会抛出StopIteration 异常,异常对象的 value 属性保存着返回的值。

==yield from 结构会在内部自动捕获 StopIteration 异常==。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把value 属性的值变成 yield from 表达式的值。

yield from基本用法

==在生成器 gen 中使用 yield from subgen() 时, subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时, gen 会阻塞,等待 subgen 终止==。

下面2个函数的作用一样,只是使用了 yield from 的更加简洁

def gen():
    for c in 'AB':
        yield c

print(list(gen()))

def gen_new():
    yield from 'AB'

print(list(gen_new()))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

==yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器,因此, x 可以是任何可迭代的对象,这只是 yield from 最基础的用法==。

yield from高级用法

==yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码==。

yield from 专门的术语

  1. 委派生成器:包含 yield from 表达式的生成器函数。
  2. 子生成器:从 yield from 中 部分获取的生成器。

图示

这里写图片描述

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

高级示例

from collections import namedtuple

ResClass = namedtuple('Res', 'count average')


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

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

    return ResClass(count, average)


# 委派生成器
def grouper(storages, key):
    while True:
        # 获取averager()返回的值
        storages[key] = yield from averager()


# 客户端代码
def client():
    process_data = {
        'boys_2': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
        'boys_1': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46]
    }

    storages = {}
    for k, v in process_data.items():
        # 获得协程
        coroutine = grouper(storages, k)

        # 预激协程
        next(coroutine)

        # 发送数据到协程
        for dt in v:
            coroutine.send(dt)

        # 终止协程
        coroutine.send(None)
    print(storages)

# run
client()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

解释: 
1. 外层 for 循环每次迭代会新建一个 grouper 实例,赋值给 coroutine 变量; grouper 是委派生成器。 
2. 调用 next(coroutine),预激委派生成器 grouper,此时进入 while True 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。 
3. 内层 for 循环调用 coroutine.send(value),直接把值传给子生成器 averager。同时,当前的 grouper 实例(coroutine)在 yield from 表达式处暂停。 
4. 内层循环结束后, grouper 实例依旧在 yield from 表达式处暂停,因此, grouper函数定义体中为 results[key] 赋值的语句还没有执行。 
5. coroutine.send(None) 终止 averager 子生成器,子生成器抛出 StopIteration 异常并将返回的数据包含在异常对象的value中,yield from 可以直接抓取 StopItration 异常并将异常对象的 value 赋值给 results[key]

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

使用案例

协程能自然地表述很多算法,例如仿真、游戏、异步 I/O,以及其他事件驱动型编程形式或协作式多任务。协程是 asyncio 包的基础构建。通过仿真系统能说明如何使用协程代替线程实现并发的活动。

在仿真领域,进程这个术语指代模型中某个实体的活动,与操作系统中的进程无关。仿真系统中的一个进程可以使用操作系统中的一个进程实现,但是通常会使用一个线程或一个协程实现。

出租车示例

import collections

# time 字段是事件发生时的仿真时间,
# proc 字段是出租车进程实例的编号,
# action 字段是描述活动的字符串。
Event = collections.namedtuple('Event', 'time proc action')


def taxi_process(proc_num, trips_num, start_time=0):
    """
    每次改变状态时创建事件,把控制权让给仿真器
    :param proc_num:
    :param trips_num:
    :param start_time:
    :return:
    """
    time = yield Event(start_time, proc_num, 'leave garage')

    for i in range(trips_num):
        time = yield Event(time, proc_num, 'pick up people')
        time = yield Event(time, proc_num, 'drop off people')

    yield Event(time, proc_num, 'go home')

# run
t1 = taxi_process(1, 1)
a = next(t1)    
print(a)    # Event(time=0, proc=1, action='leave garage')
b = t1.send(a.time + 6)
print(b)    # Event(time=6, proc=1, action='pick up people')
c = t1.send(b.time + 12)
print(c)    # Event(time=18, proc=1, action='drop off people')
d = t1.send(c.time + 1)
print(d)    # Event(time=19, proc=1, action='go home')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

模拟控制台控制3个出租车异步

import collections
import queue
import random

# time 字段是事件发生时的仿真时间,
# proc 字段是出租车进程实例的编号,
# action 字段是描述活动的字符串。
Event = collections.namedtuple('Event', 'time proc action')


def taxi_process(proc_num, trips_num, start_time=0):
    """
    每次改变状态时创建事件,把控制权让给仿真器
    :param proc_num:
    :param trips_num:
    :param start_time:
    :return:
    """
    time = yield Event(start_time, proc_num, 'leave garage')

    for i in range(trips_num):
        time = yield Event(time, proc_num, 'pick up people')
        time = yield Event(time, proc_num, 'drop off people')

    yield Event(time, proc_num, 'go home')


class SimulateTaxi(object):
    """
    模拟出租车控制台
    """

    def __init__(self, proc_map):
        # 保存排定事件的 PriorityQueue 对象,
        # 如果进来的是tuple类型,则默认使用tuple[0]做排序
        self.events = queue.PriorityQueue()
        # procs_map 参数是一个字典,使用dict构建本地副本
        self.procs = dict(proc_map)

    def run(self, end_time):
        """
        排定并显示事件,直到时间结束
        :param end_time:
        :return:
        """
        for _, taxi_gen in self.procs.items():
            leave_evt = next(taxi_gen)
            self.events.put(leave_evt)

        # 仿真系统的主循环
        simulate_time = 0
        while simulate_time < end_time:
            if self.events.empty():
                print('*** end of events ***')
                break

            # 第一个事件的发生
            current_evt = self.events.get()
            simulate_time, proc_num, action = current_evt
            print('taxi:', proc_num, ', at time:', simulate_time, ', ', action)

            # 准备下个事件的发生
            proc_gen = self.procs[proc_num]
            next_simulate_time = simulate_time + self.compute_duration()

            try:
                next_evt = proc_gen.send(next_simulate_time)
            except StopIteration:
                del self.procs[proc_num]
            else:
                self.events.put(next_evt)
        else:
            msg = '*** end of simulation time: {} events pending ***'
            print(msg.format(self.events.qsize()))

    @staticmethod
    def compute_duration():
        """
        随机产生下个事件发生的时间
        :return:
        """
        duration_time = random.randint(1, 20)
        return duration_time


# 生成3个出租车,现在全部都没有离开garage
taxis = {i: taxi_process(i, (i + 1) * 2, i * 5)
         for i in range(3)}

# 模拟运行
st = SimulateTaxi(taxis)
st.run(100)

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325981968&siteId=291194637