详解Python 协程的详细用法使用和例子

从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 —— 如果 yield 关键字后面没有表达式,那么生成器产出 None。

协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(…) 函数。

==yield 关键字甚至还可以不接收或传出数据。不管数据如何流动, yield 都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程==。

协程的生成器的基本行为

这里有一个最简单的协程代码:

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

sc = simple_coroutine()

next(sc)
sc.send('zhexiao')

解释:

1. 协程使用生成器函数定义:定义体中有 yield 关键字。
2. yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是 None —— 这个值是隐式指定的,因为 yield 关键字右边没有表达式。
3. 首先要调用 next(…) 函数,因为生成器还没启动,没在 yield 语句处暂停,所以一开始无法发送数据。
4. 调用send方法,把值传给 yield 的变量,然后协程恢复,继续执行下面的代码,直到运行到下一个 yield 表达式,或者终止。

==注意:send方法只有当协程处于 GEN_SUSPENDED 状态下时才会运作,所以我们使用 next() 方法激活协程到 yield 表达式处停止,或者我们也可以使用 sc.send(None),效果与 next(sc) 一样==。

协程的四个状态:

协程可以身处四个状态中的一个。当前状态可以使用inspect.getgeneratorstate(…) 函数确定,该函数会返回下述字符串中的一个:

1. GEN_CREATED:等待开始执行
2. GEN_RUNNING:解释器正在执行
3. GEN_SUSPENED:在yield表达式处暂停
4. GEN_CLOSED:执行结束

==最先调用 next(sc) 函数这一步通常称为“预激”(prime)协程==(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。

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

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

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. 调用 next(ag) 函数后,协程会向前执行到 yield 表达式,产出 average 变量的初始值——None。
2. 此时,协程在 yield 表达式处暂停。
3. 使用 send() 激活协程,把发送的值赋给 num,并计算出 avg 的值。
4. 使用 print 打印出 yield 返回的数据。

终止协程和异常处理

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

==终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和Ellipsis 等常量经常用作哨符值==。

显式地把异常发给协程

从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。

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()

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

he.throw(Exception) # run demo exception

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()))

==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. 外层 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')

模拟控制台控制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)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Python协程的用法和例子详解

    从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数.可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 -- 如果 yield 关键字后面没有表达式,那么生成器产出 None. 协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(-) 函数. ==yield 关键字甚至还可以不接收或传出数据.不管数据如何流动, yield 都是一种流程控制工具,使用

  • python 生成器协程运算实例

    一.yield运行方式 我们定义一个如下的生成器: def put_on(name): print("Hi {}, 货物来了,准备搬到仓库!".format(name)) while True: goods = yield print("货物[%s]已经被%s搬进仓库了."%(goods,name)) p = put_on("bigberg") #输出 G:\python\install\python.exe G:/python/untitled

  • python编程使用协程并发的优缺点

    协程 协程是一种用户态的轻量级线程,又称微线程. 协程拥有自己的寄存器上下文和栈,调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈.因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置. 优点: 1.无需线程上下文切换的开销 2.无需原子操作锁定及同步的开销 3.方便切换控制流,简化编程模型 4.高并发+高扩展性+低成本:一个CPU支持上万的协程都不

  • python简单线程和协程学习心得(分享)

    python中对线程的支持的确不够,不过据说python有足够完备的异步网络框架模块,希望日后能学习到,这里就简单的对python中的线程做个总结 threading库可用来在单独的线程中执行任意的python可调用对象.尽管此模块对线程相关操作的支持不够,但是我们还是能够用简单的线程来处理I/O操作,以减低程序响应时间. from threading import Thread import time def countdown(n): while n > 0: print('T-minus:

  • 实例讲解python中的协程

    python协程 线程和进程的操作是由程序触发系统接口,最后的执行者是系统:协程的操作则是程序员. 协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续).协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序. 协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程: event loop是协程执行的控制点, 如果你希望执行协程, 就需要用到它们. event loop提供了如下的特性: 注册.执行.取消延时调用(

  • 深入浅析python中的多进程、多线程、协程

    进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等. 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构--进程控制块. 进程就是一个程序在一个数据集上的一次动态执行过程. 进程一般由程序.数据集.进程控

  • 在python里协程使用同步锁Lock的实例

    尽管asyncio库是使用单线程来实现协程的,但是它还是并发的,乱序执行的.可以说是单线程的调度系统,并且由于执行时有延时或者I/O中断等因素,每个协程如果同步时,还是得使用一些同步对象来实现. 比如asyncio就定义了一个锁对象Lock,它一次只允许一个协程来访问共享的资源,如果多协程想访问就会阻塞起来,也就是说如果一个协程没有释放这个锁,别的协程是没有办法访问共享的资源. 例子: import asyncio import functools def unlock(lock): print

  • 在python里从协程返回一个值的示例

    下面的例子演法了怎么样从协程里返回一个值: import asyncio async def coroutine(): print('in coroutine') return 'result' event_loop = asyncio.get_event_loop() try: return_value = event_loop.run_until_complete( coroutine() ) print('it returned: {!r}'.format(return_value)) f

  • 对Python协程之异步同步的区别详解

    一下代码通过协程.多线程.多进程的方式,运行代码展示异步与同步的区别. import gevent import threading import multiprocessing # 这里展示同步和异步的性能区别,可以看到异步直接同时执行并完成, # 而同步,需要等待第一个完成后再次执行下一个,是有顺序的执行,而异步不需要 import time def task(pid): gevent.sleep(0.5) print('Task %s done' % pid) def task2(pid)

  • python协程之动态添加任务的方法

    python协程只能运行在事件循环中,但是一旦事件循环运行,又会阻塞当前任务.所以只能在当前进程中再开一个线程,这个线程的主要任务是运行事件循环,就是event_loop,因为他是一个无限循环,会阻塞当前线程. 放一个自己写的demo,注释写的很详细. 另外还有一点需要注意,一个事件循环中不能运行另外一个事件循环. 运行结果: import asyncio from threading import Thread async def production_task(): i = 0 while

  • python中协程实现TCP连接的实例分析

    在网络通信中,每个连接都必须创建新线程(或进程) 来处理,否则,单线程在处理连接的过程中, 无法接受其他客户端的连接.所以我们尝试使用协程来实现服务器对多个客户端的响应. 与单一TCP通信的构架一样,只是使用协程来实现多个任务同时进行. #服务端 import socket from gevent import monkey import gevent monkey.patch_all() def handle_conn(seObj): while True: re_Data = seObj.r

  • Python中协程用法代码详解

    本文研究的主要是python中协程的相关问题,具体介绍如下. Num01–>协程的定义 协程,又称微线程,纤程.英文名Coroutine. 首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元. 为啥说他是一个执行单元,因为他自带CPU上下文.这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程. 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的. Num02–>协程和线程的差异 那么这个过程看起来和线程差不多.其实不然, 线程切换从系统层面远不止保存和恢复 CP

  • python线程、进程和协程详解

    引言 解释器环境:python3.5.1 我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句: server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer) ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver

随机推荐