Python 的 yield 和 yield from,生成器以及 classic coroutine


先说结论

生成器的作用是:不断输出数据。
yield 有 2 个作用,第一个是用于创建生成器 generator,第二个是创建 classic coroutine 。重点关注生成器即可。
对于大多数的 Python 用户,都不需要使用 classic coroutine 。
在条件合适时,尽量使用生成器,而不要使用列表或元祖。因为生成器更节省内存。
yield from 主要用于嵌套生成器。yield 和 yield from 的区别是:

  1. yield iterable 会把 iterable 作为一个整体输出。
  2. 而 yield from iterable 则会把 iterable 拆开,将其中的每个元素逐一输出。

1. 使用生成器 generator

生成器的作用是:不断输出数据。使用生成器时,一般使用 for 循环进行遍历。生成器类似元祖和列表,可以用 for 循环把其中的元素逐个取出。如下图示例(在 JupyterLab 中的运行结果)。例子中的生成器 generator_demo 包含了 2024 和 888 两个元素。

在这里插入图片描述

1.1 不推荐使用 next 函数

另外一种使用生成器的方法是 next 函数,它的作用是每次从生成器中取出一个值。但是并不推荐使用该方法。
这是因为生成器属于 iterator,它和 iterator 一样,遍历一次之后,就不会再输出任何元素。如果此时再用 next 函数从生成器中取值,会抛出异常 StopIteration。如下示例。
如果要用 next 函数,通常要结合 try-except 使用,显得比较麻烦。
在这里插入图片描述

1.2 每次遍历时,都必须新建一个生成器

从上图例子可知,生成器只能遍历一次。因此如果要再次遍历生成器,就必须新建一个生成器。
另外,使用 for 循环遍历生成器时,并不会看到 StopIteration 异常。这是因为 for 循环直接在内部处理了 StopIteration。示例如下。
在这里插入图片描述
上图的例子中,使用了生成器表达式,新建一个生成器。这是创建生成器常用的 3 种方法之一,详见下述。


2. 创建生成器

如果要创建比较复杂的生成器,可以用生成器函数。而如果是简单的生成器,则可以用生成器表达式。
另外还可以使用内置的 itertools 模块,快速创建生成器。

2.1 用生成器函数

当一个普通函数内出现 yield 关键字时,这个函数就变成了“生成器函数”。调用这个生成器函数,就会得到一个生成器对象 generator。
大体上来说,生成器函数和普通函数一样:在函数内部,都是从上到下运行代码,并遵循 for 循环等控制流。

不同点在于,生成器函数运行到 yield 表达式时,有 3 个特殊操作:

  1. 运行完 yield 表达式后,会立刻暂停 suspend,并保存各种状态量。
  2. 会把 yield 后面的对象输出给外部的 for 循环。
  3. 当外部的 for 循环再次从生成器中取值时,生成器函数内部,会继续运行 yield 表达式后续的代码。

上面的“ yield 表达式”是 “yield foo” 的格式,foo 是一个任意对象,它将被输出给外部。一个示例如下。
在这里插入图片描述

2.2 用生成器表达式

用生成器表达式可以快速创建一个简单的生成器。
生成器表达式也叫 genexp ,是 generator expression 的简称。
genexp 是一个小括号组成的表达式,括号内可以分为 2 部分,分别是计算部分和循环部分。一个示例如下。for 的左边是计算部分,剩余的是循环部分。
在这里插入图片描述

2.2.1 对比 listcomp ,以及内存占用问题

列表解析式也叫 listcomp(list comprehension 的简写),它返回一个列表。它和 genexp 形式上基本相同,差别在于 genexp 用的是小括号,而 listcomp 用的是中括号。
在这里插入图片描述
虽然 listcomp 和 genexp 一样简单,但是一般推荐尽量使用生成器。这是因为生成器每次只生成一个元素,很节省内存。而列表/元祖等都会一次性把所有数据都创建出来。当数据很多时,列表/元祖会占用大量内存。
可以尝试运行下图的代码,体验生成器节省大量内存的效果。
在这里插入图片描述

另外,genexp 应该用于语句比较简单的场合,genexp 的计算部分和 for 循环部分都不应该超过一行,以方便其他阅读代码的人更容易理解。
当内容太多,或者是要用到嵌套循环时,就不应该使用 genexp,而应该把结构拆开,写成生成器函数的形式。关于这一点,可以参看
《Google Python Style Guide》对 listcomp 和 genexp 的讨论:https://google.github.io/styleguide/pyguide.html#27-comprehensions–generator-expressions

顺便提一下:Python 中有列表解析式和字典解析式,但是没有元祖解析式。因为用小括号替换掉列表解析式的中括号之后,就变成了生成器表达式,只返回生成器,而不是返回元祖。

2.3 用 itertools 模块

当需要使用生成器时,可以优先考虑是否可用 itertools 模块。里面有很多现成的生成器,能够节省我们编码的时间,也避免了手写生成器可能带来的 bug。
官网 itertools: https://docs.python.org/3/library/itertools.html


3. yield from

yield 和 yield from 都是用在函数中,它们都可以把函数变成生成器 generator。
这两者的区别有如下 2 点:

  1. yield iterable 会把 iterable 作为一个整体输出。例如 yield [‘a’, ‘b’] 会把列表 [‘a’, ‘b’] 整体输出。
  2. 而 yield from iterable 则会把 iterable 拆开,将其中的每个元素逐一输出。例如 yield from [‘a’, ‘b’] 会把列表拆开,先输出 ‘a’,然后再输出 ‘b’。

yield from 把 iterable 拆开的示例如下。
在这里插入图片描述
因为 yield from 有拆开 iterable 的特性,所以它很适合用于嵌套生成器。同时还可以直接获取 subgenerator 的 return 值。嵌套生成器的示例如下。

嵌套生成器一般由 3 个部分组成:client code,delegating generator 和 subgenerator。
client code,即用 for 循环遍历生成器的部分。delegating generator,即调用 subgenerator 的生成器。
在这里插入图片描述

上面例子提到: “普通生成器的 return 值无法直接获取,必须通过异常处理”,是指 classic coroutine 的用法。详见下述。


4. classic coroutine

只有少数框架开发者,才会用到 classic coroutine。如果要详细了解 classic coroutine,可以参看官网 : 《PEP 342 – Coroutines via Enhanced Generators》 https://peps.python.org/pep-0342/
对于大多数 Python 使用者,都无须使用 classic coroutine。简单了解下面的内容即可。

关于名称的说明:
Python 官网把 classic coroutine 和用于遍历的生成器 generator iterator 都统一叫做生成器 generator object。
但是这种混在一起的做法,会让使用者产生很大的困惑,因为实际上使用它们时,会有很大的差别。为了便于理解,我采用了 Luciano Ramalho 在《Fluent Python》中的方法来进行区分,即 :
1. 把用于遍历的生成器 generator iterator 叫做生成器。
2. 把使用 send(), close() 和 throw() 方法的生成器叫做 classic coroutine 。


根据 2005 年《PEP 342 – Coroutines via Enhanced Generators》的介绍,在 generator 的基础上,增加了 3 个方法 send(), close() 和 throw() ,就形成了 classic coroutine 。

和 generator 相比,在用途,创建,使用方法以及数据类型注释这四个方面,classic coroutine 都不同。

4.1 在用途上的区别:

  1. 生成器 generator 主要用于遍历,逐个输出生成器中的元素。因此也叫 generator iterator
  2. classic coroutine 则用于接收外部数据,然后在 classic coroutine 中,对接收的数据进行处理

4.2 在创建方面的区别:

  1. 创建 classic coroutine,必须把 yield 表达式赋值给一个变量。例如 foo = yield bar 的形式。
  2. 创建 generator,只需要写出 yield 表达式即可,不需要赋值。即 yield bar 即可。

4.3 在使用方法方面的区别:

  1. 使用 classic coroutine 时,需要用 send 方法把外部的数据输入给 classic coroutine 内部
  2. 使用生成器时,一般是用 for 循环,把生成器内部的数据 yield 出来给外部
  3. 可以通过 StopIteration 的 value 属性,获得 classic coroutine 的返回值 return value ,但是生成器却没有返回值(生成器的 return value 无法传递给 StopIteration)。

4.4 在数据类型注释方面的区别:

  1. 注释生成器,用 collections.abc.Iterator[YieldType] 的形式,表示一个生成器,输出 YieldType 类型的数据。
  2. 注释 classic coroutine,用 collections.abc.Generator[YieldType, SendType, ReturnType] 。
    关于类型注释还可参见:官网 https://docs.python.org/3/library/typing.html#typing.Generator

一个简单的示例如下图。
在这里插入图片描述


5. 关于 native coroutine

Python 中还有一种 native coroutine ,它是指用 async def 定义的函数。主要在 asyncio 模块中使用,出现在并发程序中。它和 thread, process 属于一组概念。
自从 Python 3.5 引入 native coroutine 之后,classic coroutine 就变得越来越不重要。如果直接叫 coroutine,通常都是指 asyncio 模块中的这个 native coroutine。对于大多数 Python 用户,如果用到 coroutine,一般也都是 native coroutine 。


6. 参考资料

  1. 官网的 yield 表达式:https://docs.python.org/3/reference/expressions.html#yield-expressions

  2. 《Fluent Python》https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/

官网有一张图如下,大致介绍了生成器的发展历程:
PEP 255:生成器诞生。
PEP 342:classic coroutine 诞生。
PEP 380:引入 yield from。
PEP 525:引入了异步生成器 asynchronous generator 。异步生成器是在 native coroutine 中,使用了 yield 关键字。
《PEP 525 – Asynchronous Generators》 https://peps.python.org/pep-0525/#support-for-asynchronous-iteration-protocol

在这里插入图片描述


—————————— 本文结束 ——————————

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值