在这篇文章中,我将讨论一下什么是生成器,并将其与协程做出对比。但是为了理解这两个概念(生成器与协程),我们需要先深入了解一下迭代器的概念。

我们将会讨论...

  • 迭代器

    • 为什么使用迭代器?

    • 迭代器实现

    • 迭代器示例

  • 生成器

    • 为什么使用生成器?

    • 生成器实现

    • 生成器示例

    • 生成器表达式

    • 内置生成器(即 yield from)

  • 协程

    • 为什么使用协程?

    • 协程实现

    • 协程示例

    • Asyncio:基于生成器的协程

    • Asyncio:定义异步协程

    • 协程类型

    • 混用

上面每一部分内容都是相衔接的,因此最好是按照章节定义的顺序阅读。除非是你已经熟悉前面的部分,并且想要跳过这部分。

概要

下面我们将要讨论的内容概述如下:

  • 迭代器允许迭代自定义对象

  • 生成器是基于迭代器的(只是减少了部分样板)

  • 生成器表达式是更简洁的生成器

  • 协程是生成器,不过它使用 yield 接收值

  • 协程可以暂定与恢复执行(非常适合并发程序)

迭代器

根据 Python 官方词汇表,「生成器」是:一个表示数据流的对象。

为什么使用迭代器?

迭代器很有用,因为他允许我们使用 Python 的 for-in 语法迭代任何自定义的对象。就像迭代一些内部列表和字典类型一样,使用 for-in 迭代生成器内部的数据。

重要的是,(我们可以看得到)迭代器的内存效率很高,它一次只会处理一个元素。因此,我们可以拥有一个具有无限序列元素的迭代器,然后还永远不用担心内存耗尽问题。

迭代器实现

一个迭代器(通常)是一个同时实现了 __iter__ 和 __next__ "dunder" 方法的对象,虽然 __next__ 不是一定要和 __iter__ 定义在同一位置。这一点稍后作出说明。

「迭代器」本质上只是一些数据的容器。这个「容器」必须包含 __iter__ 方法,根据协议文档,这个方法应该返回一个迭代器对象(即包含 __next__ 函数的内容)。这里是根据 __next__ 进行推移并收集数据。

因此,你可以设计一个同时包含 __iter__ 和 __next__ 方法的类(如下图所示),或者你可能希望将 __next__方法单独定义为类的一部分(你可以定义为任何你认为最适合你的项目的方式)。

注意:Python 文档中 collections.abc 表明了 Python 中的其他「协议」以及它们所需的各种方法(参考我之前关于协议与抽象类的帖子)。如果你不熟悉各种 "dunder" 方法,那么推荐你一个不错的帖子:魔术方法指南

通过实现这两个方法,Python 能够对「集合」进行迭代。至于「集合」是什么不重要,只要迭代器对象定义的行为让 Python 知道如何迭代它就可以了。

迭代器示例

下面是一个精心设计的示例,演示了如何创建一个迭代器对象。这个示例中,我们将字符串列表传递给类构造函数,且类实现了允许 for-in 迭代该数据的相关方法:

注意:抛出 StopInteration 异常是正确实现迭代器的一项要求。

通过这个实现示例,我们可以使用 iter 和 next 方法来手动迭代 Foo 类。如下所示:

注意:这里 iter(foo) 与 foo.__iter__() 相同,而 next(iterator) 与 iterator.__next__() 相同。而之所以这样,是因为这是标准库提供的基本语法糖,这样可以使我们的代码看起来更美观。

这种类型的迭代器称为「基于类的迭代器」,而且这并不是实现可迭代对象的唯一方法。生成器和生成器表达式(参考以下章节)是另外迭代对象的方法。

我们还可以使用 list 函数实现完整的集合,如下所示:

注意:执行这个操作的时候需要小心,因为如果迭代器生成了无限数量的元素,这将消耗尽程序的内存。

生成器

根据 Python 官方文档,「生成器」提供了....

一种实现迭代器协议的更便捷的方式。如果容器对象的 __iter__() 函数被实现为生成器,它会自动返回一个迭代器对象。

为什么使用生成器?

生成器为创建一个简单的迭代器提供了很好的语法糖,并且还有助于减少一些迭代一些内容必须的样板代码。

生成器有助于减少创建「基于类」的迭代器关联的样板代码,因为它们的目的是专注与处理「状态管理」逻辑,除非你想自己写这部分代码。

生成器实现

生成器是一个返回「生成器迭代器」的方法,因此它的行为类似于 __iter__ 的工作方式(记住,它返回的是一个迭代器)。

实际上,生成器是迭代器的一个子类。生成器函数本身利用 yield 语句将控制权返回给生成器函数的调用者。

然后,调用方可以使用 for-in 语句或者 next 方法(就像我们之前看到的「基于类」的迭代器示例)进行推进迭代器,这再次强调了生成器实际上是迭代器的一个子类。

当生成器执行到 yield 时,它实际上会暂停运行这个时间点的函数,并返回一个值。再调用 next(或者是作为 for-in)会向前移动该函数,直到完成生成器函数或遇到函数中的下一个 yield 声明处停止。

生成器示例

下面的示例会按顺序打印:a,b,c:

如果我们使用 next() 方法,则需要这么做:

注意:与之前创建的自定义的「基于类」的迭代器相比,这极大地减少了我们的样板代码,因为无需在类实例上定义 __iter__ 和 __next__ 方法(也不需要自己管理任何状态)。我们只是简单地调用 yield。

如果我们的场景十分简单,那么使用生成器就可以了。不然,如果我们有很多具体的逻辑需要执行,我们可以需要自定义一个「基于类」的迭代器。

记住,迭代器(及其扩展生成器)具有极高地内存效率,因此我们可以使用一个生成器来生成无限数量的元素。就像这样:

因此,综上所述,在生成器函数上使用 list() 要小心(参考下面的示例),因为这会实现整个集合,并有可能消耗全部内存。

生成器表达式

根据官方 PEP 289 文档的描述,生成器表达式...

生成器表达式是一个类似列表推导式与生成器的高性能、高内存效率的概念。

本质上讲,它是使用与列表推导式非常相似的语法创建生成器的一种方式。

下面是一个生成器函数的示例,该函数将打印 5 次 "foo":

这个实现了与使用生成器一样的功能:

生成器表达式的语法也与推导式的语法非常类似,只不过我们使用的标识字符是 () 而不是 [] 或 {} :

注意:尽管没有演示出来,但是由于支持 if 操作,你也可以使用 filter 操作。

内置生成器(即 yield from)

Python 3.3 提供了 yield from 语句段,它提供了一些处理内置生成器的语法糖。

举一个例子,假设我们没有 yield from :

注意这里我们有两个独立的 for-in 循环,每一个都有一个内置的生成器:

现在我们来开始使用 yield from:

好吧,所以这不完全算是一个突破性的功能,但是如果之前被 yield from 搞晕了的话,使用现在这个包装一下 for-in 语法,会显得简单一些。

尽管不带有 yield from 的代码不值得我们关注,我们仍然可以使用 itertool 模块中的 chain() 方法来对我们的原始代码进行整理,就像这样:

注意:想要了解 Python 语言中关于 yield from 及其原理的更多信息可以参考 PEP 380

协程

协程(就 Python 而言)在之前一直被设计为生成器的扩展功能。

协程是计算机程序组件,通过允许挂起和恢复执行来描述非抢占式多任务处理的子例程。    ---- 维基百科

为什么使用协程?

因为协程可以暂停和恢复执行程序上下文,所以它非常适合处理并发程序,同时它也可以确定在某一时刻将上下文从代码的一个点切换到另一个点。

这是为什么处理比如事件循环(基于 Python 的 asyncio)的概念是通常使用协程的原因。

协程实现

生成器使用 yield 关键字在方法内的某个时间点返回一个值,但是使用协程时,yield 指令也可以用于 = 运算法的右侧,以表示它将在该时间点接收值。

下面是一个协程的示例。记住,协程也是一个生成器,因此你可以在我们的示例中看到生成器相关的功能(比如 yield 和 next() 方法):

注意:为方便理解,请参考代码注释。

注意:core 是一个标识符,通常用于引用一个协程。有关其他协程中可用的方法,请参考文档。

下面的协程示例中使用 yield 在返回给调用方一个值之前,调用方通过 .send() 函数接收这个值。

在上面的示例中,你可以看到,我们将生成器协程运行到第一个 yield 语句(使用 next(core))时,值 "beep"被返回并打印。

Asyncio:基于生成器的协程

当 asyncio 模块首次发布时,它是不支持 async/await 语法的。因此在使用时,为了确保旧代码也可以并发(即等待)运行,需要使用 asyncio.coroutine 装饰器方法,来保证与新的 async/await 语法兼容。

注意:有关这个(自 Python 3.10)弃用的功能,以及一些其他的诸如 asyncio.iscoroutine 这样基于生成器的协程,请参考文档。

最初基于生成器的协程意味着使用 asyncio 的代码可以使用 yield from 在Feature等待其它协程。

下面的示例演示了如何将新的协程与旧的协程一起使用:

Asyncio:定义异步协程

使用 await def 实现的协程是基于较新的 __await__ "dunder" 方法(参考文档),而基于生成器的协程则是使用旧式「生成器」实现的。

协程类型

这意味着「协程」在不同的上下文中可以有多个内容。现在有:

  • 简单协程:传统的生成器协程(没有异步 IO)

  • 生成器协程:使用旧式的 asyncio 实现的异步 IO

  • 原生协程:使用最新的 async/await 实现的协程

混用

Python 提供的几个有趣的装饰器功能上可能有点混乱。因为这些函数有些功能看似是重叠的。

他们并不是重叠,但是可以一起使用:

  • types.coroutine:将生成器转化为协程。

  • asyncio.coroutine:抽象确保 asyncio 的兼容性。

注意:正如我们刚刚看到的,asyncio.coroutine 实际上是调用了 types.coroutine。处理异步代码时最好是使用前者。

更具体来说,如果我们去看一下 asyncio.coroutine 代码的实现,就可以知道:

  • 如果装饰函数已经是一个协程,直接返回它就可以了。

  • 如果装饰函数是生成器,则将其转换为协程(使用 types.coroutine)。

  • 否则,将装饰函数包装起来,以便其在转化为协程的时候,可以等待任何可等待的值。

关于 types.coroutine 比较有趣的是,如果你的装饰函数移除了对 yield 的一些引用,那么这个函数将会立即执行,而不再是返回生成器。有关这种操作的给更多信息,可以参考 Stack Overflow 上的回答。

英文原文:https://www.integralist.co.uk/posts/python-generators/ 
译者:居老师的龙尾巴

协程asyncio_迭代器,生成器,协程相关推荐

  1. 协程asyncio_初识asyncio协程

    初识asyncio协程 一.基本概念 ​ 要想了解学习协程相关知识要先对以下几个概念先行了解: 阻塞 ​ 阻塞状态是指程序未得到某所需计算资源时的挂起状态,简单说就是程序在等待某个操作未执行完前无法执 ...

  2. python的装饰器、迭代器、yield_python装饰器,迭代器,生成器,协程

    python装饰器[1] 首先先明白以下两点 #嵌套函数 defout1():definner1():print(1234) inner1()#当没有加入inner时out()不会打印输出1234,当 ...

  3. 【Python】【容器 | 迭代对象 | 迭代器 | 生成器 | 生成器表达式 | 协程 | 期物 | 任务】...

    Python 的 asyncio 类似于 C++ 的 Boost.Asio. 所谓「异步 IO」,就是你发起一个 IO 操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知. Asyn ...

  4. Python 中的黑暗角落(二):生成器协程的调度问题

    前作介绍了 Python 中的 yield 关键字.此篇介绍如何使用 yield 表达式,在 Python 中实现一个最基本的协程调度示例,避免 I/O 操作占用大量 CPU 计算时间. 协程及其特点 ...

  5. linux的进程/线程/协程系列5:协程的发展复兴与实现现状

    协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...

  6. php携程语比,PHP 协程

    理解生成器 参考官方文档:Generators 生成器让我们快速.简单地实现一个迭代器,而不需要创建一个实现了Iterator接口的类后,再实例化出一个对象. 一个生成器长什么样?如下 1 2 3 4 ...

  7. python协程详解_python协程详解

    原博文 2019-10-25 10:07 − # python协程详解 ![python协程详解](https://pic2.zhimg.com/50/v2-9f3e2152b616e89fbad86 ...

  8. python中协程的理解_python协程的理解

    一.介绍 什么是并发? 并发的本质就是切换+保存状态 cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制): 1.任务发生阻塞 2.计算任务时间过长,需要让出cpu给高 ...

  9. 进程 线程 协程_进程 线程 协程 管程 纤程 概念对比理解

    不知道是不是我自己本身就有那么一丝丝的密集恐惧,把这么一大堆看起来很相似很相关的概念放在一起,看起来是有点麻,捋一捋感觉舒服多了. 相关概念 任务.作业(Job,Task,Schedule) 在进程的 ...

  10. Kotlin 之 协程(四)协程并发

    认识channel channel是一个并发安全的队列,可以连接协程,实现不同协程的通信. Library中定义了几种类型的Channel. 它们在内部能够存储多种元素,只是在send调用是否能够挂起 ...

最新文章

  1. Python与机器视觉(x)图像修复
  2. Java Web前后端分离的思考与实践
  3. ZED2+ORB_SLAM3+视觉惯性轨迹保存
  4. 达观数据个性化推荐系统实践
  5. 金融数据获取的api接口
  6. C#从入门到精通之第一篇: C#概述与入门
  7. Linux设备驱动 | LED字符设备驱动(platform平台总线)
  8. Pytorch实战__反向攻击(Adversarial Attack)
  9. 02 必备SQL和表关系及授权
  10. 《SteamVR实战之PMCore》(Yanlz+Unity+XR+SteamVR+VR+AR+MR+Valve+Oculus+立钻哥哥+==)
  11. 如何将一串数字用函数的方法倒过来(C语言)
  12. linux 内核调试 booting the kernel.,Linux无法启动解决 booting the kernel.
  13. 斗战神单机版正在连接服务器,斗战神登录卡在这里,又不提示登录失败或者连接超时什......
  14. 前缀、真前缀、后缀、真后缀
  15. elf文件反编译C语言,图文并茂,讲透C语言静态链接,ELF文件篇
  16. 理解Segment Routing和SDWAN
  17. js中对象数组根据对象id分组并转map
  18. 啁啾信号chirp(扫频余弦信号)
  19. confusion_matrix函数
  20. SPSS折线图【012-2期】

热门文章

  1. restful架构风格设计准则(五)用户认证和session管理
  2. 六款优秀的 Linux 基准测试工具
  3. WampServer 给电脑搭建apache服务器和php环境
  4. 备忘: Visual Studio 2013 VC++ IDE 使用小贴示。
  5. 读取文件,解决中文乱码问题
  6. TOMCAT内存大小调整
  7. logback-spring.xml文件配置
  8. Github上托管项目
  9. vue.js引入外部CSS样式和外部JS文件的方法
  10. ftp服务器文件上传代码,Java上传文件FTP服务器代码