百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

python协程1:yield的使用

bigegpt 2024-09-17 12:35 4 浏览

协程定义

协程的底层架构是在pep342 中定义,并在python2.5 实现的。

python2.5 中,yield关键字可以在表达式中使用,而且生成器API中增加了 .send(value)方法。生成器可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield表达式的值。

协程是指一个过程,这个过程与调用方协作,产出有调用方提供的值。因此,生成器可以作为协程使用。

除了 .send(...)方法,pep342 和添加了 .throw(...)(让调用方抛出异常,在生成器中处理)和.close()(终止生成器)方法。

python3.3后,pep380对生成器函数做了两处改动:

  • 生成器可以返回一个值;以前,如果生成器中给return语句提供值,会抛出SyntaxError异常。

  • 引入yield from 语法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去之前把生成器的工作委托给子生成器所需的大量模板代码。

协程生成器的基本行为

首先说明一下,协程有四个状态,可以使用inspect.getgeneratorstate(...)函数确定:

  • GEN_CREATED # 等待开始执行

  • GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)

  • GEN_SUSPENDED # 在yield表达式处暂停

  • GEN_CLOSED # 执行结束

#! -*- coding: utf-8 -*-

import inspect

# 协程使用生成器函数定义:定义体中有yield关键字。

def simple_coroutine(): print('-> coroutine started') # yield 在表达式中使用;如果协程只需要从客户那里接收数据,yield关键字右边不需要加表达式(yield默认返回None) x = yield print('-> coroutine received:', x) my_coro = simple_coroutine()# 和创建生成器的方式一样,调用函数得到生成器对象。

print(inspect.getgeneratorstate(my_coro))

# 协程处于 GEN_CREATED (等待开始状态)my_coro.send(None)

# 首先要调用next()函数,因为生成器还没有启动,没有在yield语句处暂停,所以开始无法发送数据

# 发送 None 可以达到相同的效果 my_coro.send(None)

next(my_coro)# 此时协程处于 GEN_SUSPENDED (在yield表达式处暂停)

print(inspect.getgeneratorstate(my_coro))

# 调用这个方法后,协程定义体中的yield表达式会计算出42;现在协程会恢复,一直运行到下一个yield表达式,或者终止。

my_coro.send(42) print(inspect.getgeneratorstate(my_coro))

运行上述代码,输出结果如下

GEN_CREATED
-> coroutine started
GEN_SUSPENDED
-> coroutine received: 42# 这里,控制权流动到协程定义体的尾部,导致生成器像往常一样抛出StopIteration异常

Traceback (most recent call last): File "/Users/gs/coroutine.py", line 18, in <module> my_coro.send(42) StopIteration

send方法的参数会成为暂停yield表达式的值,所以,仅当协程处于暂停状态是才能调用send方法。

如果协程还未激活(GEN_CREATED 状态)要调用next(my_coro) 激活协程,也可以调用my_coro.send(None)

如果创建协程对象后立即把None之外的值发给它,会出现下述错误:

>>> my_coro = simple_coroutine()

>>> my_coro.send(123) Traceback (most recent call last): File "/Users/gs/coroutine.py", line 14, in <module> my_coro.send(123) TypeError: can't send non-None value to a just-started generator

仔细看错误消息

can't send non-None value to a just-started generator

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

再看一个两个值得协程

def simple_coro2(a):
 print('-> coroutine started: a =', a)
 b = yield a
 print('-> Received: b =', b)
 c = yield a + b
 print('-> Received: c =', c)

my_coro2 = simple_coro2(14)
print(inspect.getgeneratorstate(my_coro2))# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_CREATED (协程未启动)

next(my_coro2)

# 向前执行到第一个yield 处 打印 “-> coroutine started: a = 14”

# 并且产生值 14 (yield a 执行 等待为b赋值)

print(inspect.getgeneratorstate(my_coro2))

# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)

my_coro2.send(28)

# 向前执行到第二个yield 处 打印 “-> Received: b = 28”

# 并且产生值 a + b = 42(yield a + b 执行 得到结果42 等待为c赋值)

print(inspect.getgeneratorstate(my_coro2))

# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)

my_coro2.send(99)

# 把数字99发送给暂停协程,计算yield 表达式,得到99,然后把那个数赋值给c 打印 “-> Received: c = 99”

# 协程终止,抛出StopIteration

运行上述代码,输出结果如下

GEN_CREATED
-> coroutine started: a = 14

GEN_SUSPENDED -> Received: b = 28

-> Received: c = 99

Traceback (most recent call last): File "/Users/gs/coroutine.py", line 37, in <module> my_coro2.send(99) StopIteration

simple_coro2 协程的执行过程分为3个阶段,如下图所示

  1. 调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字14.

  2. 调用my_coro2.send(28),把28赋值给b,打印第二个消息,然后执行 yield a + b 产生数字42

  3. 调用my_coro2.send(99),把99赋值给c,然后打印第三个消息,协程终止。

使用装饰器预激协程

我们已经知道,协程如果不预激,不能使用send() 传入非None 数据。所以,调用my_coro.send(x)之前,一定要调用next(my_coro)。

为了简化,我们会使用装饰器预激协程。

from functools import wraps

def coroutinue(func): ''' 装饰器: 向前执行到第一个`yield`表达式,预激`func` :param func: func name :return: primer ''' @wraps(func) def primer(*args, **kwargs): # 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。 gen = func(*args, **kwargs) # 调用被被装饰函数,获取生成器对象 next(gen) # 预激生成器 return gen # 返回生成器 return primer

# 使用方法如下

@coroutinue

def simple_coro(a): a = yield

simple_coro(12) # 已经预激

终止协程和异常处理

协程中,为处理的异常会向上冒泡,传递给next函数或send方法的调用方,未处理的异常会导致协程终止。

看下边这个例子

#! -*- coding: utf-8 -*-

from functools import wraps

def coroutinue(func): ''' 装饰器: 向前执行到第一个`yield`表达式,预激`func` :param func: func name :return: primer '''

@wraps(func) def primer(*args, **kwargs): # 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。 gen = func(*args, **kwargs) # 调用被被装饰函数,获取生成器对象 next(gen) # 预激生成器 return gen # 返回生成器 return primer

@coroutinue

def averager(): # 使用协程求平均值 total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count coro_avg = averager() print(coro_avg.send(40)) print(coro_avg.send(50)) print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。

执行上述代码结果如下

40.0

45.0

Traceback (most recent call last): File "/Users/gs/coro_exception.py", line 37, in <module> print(coro_avg.send('123')) File "/Users/gs/coro_exception.py", line 30, in averager total += term TypeError: unsupported operand type(s) for +=: 'float' and 'str'

出错的原因是发送给协程的'123'值不能加到total变量上。

出错后,如果再次调用 coro_avg.send(x) 方法 会抛出 StopIteration 异常。

由上边的例子我们可以知道,如果想让协程退出,可以发送给它一个特定的值。比如None和Ellipsis。(推荐使用Ellipsis,因为我们不太使用这个值)

从Python2.5 开始,我们可以在生成器上调用两个方法,显式的把异常发给协程。

这两个方法是throw和close。

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

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

generator.close()

生成器在暂停的yield表达式处抛出GeneratorExit异常。

如果生成器没有处理这个异常或者抛出了StopIteration异常,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。

示例: 使用close和throw方法控制协程。

import inspect

class DemoException(Exception): pass

@coroutinue

def exc_handling(): print('-> coroutine started')

while True:

try: x = yield except DemoException: print('*** DemoException handled. Conginuing...')

else: # 如果没有异常显示接收到的值 print('--> coroutine received: {!r}'.format(x))

raise RuntimeError('This line should never run.') # 这一行永远不会执行

exc_coro = exc_handling() exc_coro.send(11) exc_coro.send(12) exc_coro.send(13) exc_coro.close() print(inspect.getgeneratorstate(exc_coro))

raise RuntimeError('This line should never run.') 永远不会执行,因为只有未处理的异常才会终止循环,而一旦出现未处理的异常,协程会立即终止。

执行上述代码得到结果为:

-> coroutine started
--> coroutine received: 11

--> coroutine received: 12

--> coroutine received: 13

GEN_CLOSED # 协程终止

上述代码,如果传入DemoException,协程不会中止,因为做了异常处理。

exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.throw(DemoException) # 协程不会中止,但是如果传入的是未处理的异常,协程会终止

print(inspect.getgeneratorstate(exc_coro)) exc_coro.close() print(inspect.getgeneratorstate(exc_coro))

## output-> coroutine started --> coroutine received: 11

--> coroutine received: 12

--> coroutine received: 13

*** DemoException handled. Conginuing... GEN_SUSPENDED GEN_CLOSED

如果不管协程如何结束都想做些处理工作,要把协程定义体重的相关代码放入try/finally块中。

@coroutinue

def exc_handling(): print('-> coroutine started')

try:

while True:

try: x = yield except DemoException: print('*** DemoException handled. Conginuing...')

else: # 如果没有异常显示接收到的值 print('--> coroutine received: {!r}'.format(x))

finally: print('-> coroutine ending')

上述部分介绍了:

  • 生成器作为协程使用时的行为和状态

  • 使用装饰器预激协程

  • 调用方如何使用生成器对象的 .throw(...)和.close() 方法控制协程

下一部分将介绍:

  • 协程终止时如何返回值

  • yield新句法的用途和语义

原文出处:goodspeed / 四月

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...